mcp-souschef 2.0.1__py3-none-any.whl → 2.1.2__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.
@@ -0,0 +1,555 @@
1
+ """Validation framework for Chef-to-Ansible conversions."""
2
+
3
+ import ast
4
+ import re
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+
9
+ class ValidationLevel(str, Enum):
10
+ """Severity level for validation issues."""
11
+
12
+ ERROR = "error"
13
+ WARNING = "warning"
14
+ INFO = "info"
15
+
16
+
17
+ class ValidationCategory(str, Enum):
18
+ """Category of validation check."""
19
+
20
+ SYNTAX = "syntax"
21
+ SEMANTIC = "semantic"
22
+ BEST_PRACTICE = "best_practice"
23
+ SECURITY = "security"
24
+ PERFORMANCE = "performance"
25
+
26
+
27
+ class ValidationResult:
28
+ """
29
+ Result from a validation check.
30
+
31
+ Attributes:
32
+ level: Severity level of the validation issue.
33
+ category: Category of the validation check.
34
+ message: Human-readable message describing the issue.
35
+ location: Optional location information (line number, resource name, etc.).
36
+ suggestion: Optional suggestion for fixing the issue.
37
+
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ level: ValidationLevel,
43
+ category: ValidationCategory,
44
+ message: str,
45
+ location: str | None = None,
46
+ suggestion: str | None = None,
47
+ ) -> None:
48
+ """
49
+ Initialize validation result.
50
+
51
+ Args:
52
+ level: Severity level.
53
+ category: Validation category.
54
+ message: Issue description.
55
+ location: Optional location information.
56
+ suggestion: Optional fix suggestion.
57
+
58
+ """
59
+ self.level = level
60
+ self.category = category
61
+ self.message = message
62
+ self.location = location
63
+ self.suggestion = suggestion
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ """
67
+ Convert validation result to dictionary.
68
+
69
+ Returns:
70
+ Dictionary representation of the validation result.
71
+
72
+ """
73
+ result = {
74
+ "level": self.level.value,
75
+ "category": self.category.value,
76
+ "message": self.message,
77
+ }
78
+ if self.location:
79
+ result["location"] = self.location
80
+ if self.suggestion:
81
+ result["suggestion"] = self.suggestion
82
+ return result
83
+
84
+ def __repr__(self) -> str:
85
+ """
86
+ Return string representation of validation result.
87
+
88
+ Returns:
89
+ Formatted string representation.
90
+
91
+ """
92
+ parts = [f"[{self.level.value.upper()}] [{self.category.value}] {self.message}"]
93
+ if self.location:
94
+ parts.append(f" Location: {self.location}")
95
+ if self.suggestion:
96
+ parts.append(f" Suggestion: {self.suggestion}")
97
+ return "\n".join(parts)
98
+
99
+
100
+ class ValidationEngine:
101
+ """
102
+ Engine for validating Chef-to-Ansible conversions.
103
+
104
+ Provides validation across syntax, semantics, best practices, security,
105
+ and performance categories.
106
+ """
107
+
108
+ def __init__(self) -> None:
109
+ """Initialize validation engine."""
110
+ self.results: list[ValidationResult] = []
111
+
112
+ def validate_conversion(
113
+ self, conversion_type: str, result: str
114
+ ) -> list[ValidationResult]:
115
+ """
116
+ Validate a Chef-to-Ansible conversion.
117
+
118
+ Args:
119
+ conversion_type: Type of conversion ('recipe', 'resource', etc.).
120
+ result: Resulting Ansible code or configuration.
121
+
122
+ Returns:
123
+ List of validation results.
124
+
125
+ """
126
+ self.results = []
127
+
128
+ if conversion_type == "resource":
129
+ self._validate_resource_conversion(result)
130
+ elif conversion_type == "recipe":
131
+ self._validate_recipe_conversion(result)
132
+ elif conversion_type == "template":
133
+ self._validate_template_conversion(result)
134
+ elif conversion_type == "inspec":
135
+ self._validate_inspec_conversion(result)
136
+ else:
137
+ self.results.append(
138
+ ValidationResult(
139
+ ValidationLevel.WARNING,
140
+ ValidationCategory.SYNTAX,
141
+ f"Unknown conversion type: {conversion_type}",
142
+ )
143
+ )
144
+
145
+ return self.results
146
+
147
+ # Alias for backward compatibility
148
+ validate_converted_content = validate_conversion
149
+
150
+ def _add_result(
151
+ self,
152
+ level: ValidationLevel,
153
+ category: ValidationCategory,
154
+ message: str,
155
+ location: str | None = None,
156
+ suggestion: str | None = None,
157
+ ) -> None:
158
+ """
159
+ Add a validation result.
160
+
161
+ Args:
162
+ level: Severity level.
163
+ category: Validation category.
164
+ message: Issue description.
165
+ location: Optional location.
166
+ suggestion: Optional suggestion.
167
+
168
+ """
169
+ self.results.append(
170
+ ValidationResult(level, category, message, location, suggestion)
171
+ )
172
+
173
+ def _validate_resource_conversion(self, result: str) -> None:
174
+ """
175
+ Validate Chef resource to Ansible task conversion.
176
+
177
+ Args:
178
+ result: Resulting Ansible task.
179
+
180
+ """
181
+ # Syntax validation
182
+ self._validate_yaml_syntax(result)
183
+ self._validate_ansible_module_exists(result)
184
+
185
+ # Semantic validation
186
+ self._validate_idempotency(result)
187
+ self._validate_resource_dependencies(result)
188
+
189
+ # Best practice validation
190
+ self._validate_task_naming(result)
191
+ self._validate_module_usage(result)
192
+
193
+ def _validate_recipe_conversion(self, result: str) -> None:
194
+ """
195
+ Validate Chef recipe to Ansible playbook conversion.
196
+
197
+ Args:
198
+ result: Resulting Ansible playbook.
199
+
200
+ """
201
+ # Syntax validation
202
+ self._validate_yaml_syntax(result)
203
+
204
+ # Semantic validation
205
+ self._validate_variable_usage(result)
206
+ self._validate_handler_definitions(result)
207
+
208
+ # Best practice validation
209
+ self._validate_playbook_structure(result)
210
+
211
+ def _validate_template_conversion(self, result: str) -> None:
212
+ """
213
+ Validate Chef template to Jinja2 conversion.
214
+
215
+ Args:
216
+ result: Resulting Jinja2 template.
217
+
218
+ """
219
+ # Syntax validation
220
+ self._validate_jinja2_syntax(result)
221
+
222
+ # Semantic validation
223
+ self._validate_variable_references(result)
224
+
225
+ def _validate_inspec_conversion(self, result: str) -> None:
226
+ """
227
+ Validate InSpec to test framework conversion.
228
+
229
+ Args:
230
+ result: Resulting test code.
231
+
232
+ """
233
+ # Syntax validation
234
+ if "import pytest" in result:
235
+ # Testinfra format
236
+ self._validate_python_syntax(result)
237
+ elif "---" in result:
238
+ # Ansible assert format
239
+ self._validate_yaml_syntax(result)
240
+
241
+ def _validate_yaml_syntax(self, yaml_content: str) -> None:
242
+ """
243
+ Validate YAML syntax.
244
+
245
+ Args:
246
+ yaml_content: YAML content to validate.
247
+
248
+ """
249
+ try:
250
+ import yaml
251
+ except ImportError:
252
+ # YAML library unavailable, skip validation
253
+ return
254
+
255
+ try:
256
+ yaml.safe_load(yaml_content)
257
+ except yaml.YAMLError as e:
258
+ self._add_result(
259
+ ValidationLevel.ERROR,
260
+ ValidationCategory.SYNTAX,
261
+ f"Invalid YAML syntax: {e}",
262
+ suggestion="Check YAML indentation and structure",
263
+ )
264
+
265
+ def _validate_ansible_module_exists(self, task: str) -> None:
266
+ """
267
+ Validate that Ansible module exists.
268
+
269
+ Args:
270
+ task: Ansible task YAML.
271
+
272
+ """
273
+ # Extract module name from task
274
+ module_pattern = r"ansible\.builtin\.(\w+):"
275
+ match = re.search(module_pattern, task)
276
+ if match:
277
+ module_name = match.group(1)
278
+ # Check if it's a known module
279
+ known_modules = {
280
+ "package",
281
+ "apt",
282
+ "yum",
283
+ "dnf",
284
+ "service",
285
+ "systemd",
286
+ "template",
287
+ "file",
288
+ "copy",
289
+ "command",
290
+ "shell",
291
+ "user",
292
+ "group",
293
+ "cron",
294
+ "lineinfile",
295
+ "blockinfile",
296
+ "assert",
297
+ "debug",
298
+ "set_fact",
299
+ "include_tasks",
300
+ "import_tasks",
301
+ }
302
+ if module_name not in known_modules:
303
+ self._add_result(
304
+ ValidationLevel.WARNING,
305
+ ValidationCategory.SYNTAX,
306
+ f"Unknown Ansible module: {module_name}",
307
+ suggestion="Verify module name and Ansible version support",
308
+ )
309
+
310
+ def _validate_idempotency(self, task: str) -> None:
311
+ """
312
+ Validate task idempotency.
313
+
314
+ Args:
315
+ task: Ansible task YAML.
316
+
317
+ """
318
+ # Check for command/shell without changed_when
319
+ if (
320
+ "ansible.builtin.command:" in task or "ansible.builtin.shell:" in task
321
+ ) and "changed_when:" not in task:
322
+ self._add_result(
323
+ ValidationLevel.WARNING,
324
+ ValidationCategory.BEST_PRACTICE,
325
+ "Command/shell task without changed_when may report incorrect changes",
326
+ suggestion='Add changed_when: "false" or appropriate condition',
327
+ )
328
+
329
+ def _validate_resource_dependencies(self, task: str) -> None:
330
+ """
331
+ Validate resource dependencies and ordering.
332
+
333
+ Args:
334
+ task: Ansible task YAML.
335
+
336
+ """
337
+ # Check for service before package
338
+ if "ansible.builtin.service:" in task and "state:" in task:
339
+ self._add_result(
340
+ ValidationLevel.INFO,
341
+ ValidationCategory.SEMANTIC,
342
+ "Service task should have dependency on package installation",
343
+ suggestion="Consider adding handler or dependency chain",
344
+ )
345
+
346
+ def _validate_task_naming(self, task: str) -> None:
347
+ """
348
+ Validate task naming conventions.
349
+
350
+ Args:
351
+ task: Ansible task YAML.
352
+
353
+ """
354
+ # Extract task name
355
+ name_pattern = r"name:\s*([^\n]+)"
356
+ match = re.search(name_pattern, task)
357
+ if match:
358
+ task_name = match.group(1).strip("\"'")
359
+ # Check naming conventions
360
+ if not task_name:
361
+ self._add_result(
362
+ ValidationLevel.WARNING,
363
+ ValidationCategory.BEST_PRACTICE,
364
+ "Task has empty name",
365
+ suggestion="Provide descriptive task name",
366
+ )
367
+ elif len(task_name) < 10:
368
+ self._add_result(
369
+ ValidationLevel.INFO,
370
+ ValidationCategory.BEST_PRACTICE,
371
+ "Task name is very short",
372
+ suggestion="Consider more descriptive task name",
373
+ )
374
+
375
+ def _validate_module_usage(self, task: str) -> None:
376
+ """
377
+ Validate proper module usage.
378
+
379
+ Args:
380
+ task: Ansible task YAML.
381
+
382
+ """
383
+ # Check for deprecated patterns
384
+ if "ansible.builtin.file:" in task and "creates:" in task:
385
+ self._add_result(
386
+ ValidationLevel.WARNING,
387
+ ValidationCategory.BEST_PRACTICE,
388
+ "Using 'creates' with file module is unusual",
389
+ suggestion="Consider using appropriate state parameter",
390
+ )
391
+
392
+ def _extract_jinja2_variables(self, content: str) -> set[str]:
393
+ """
394
+ Extract Jinja2 variable references from content.
395
+
396
+ Args:
397
+ content: Content containing Jinja2 variables.
398
+
399
+ Returns:
400
+ Set of variable names found.
401
+
402
+ """
403
+ var_pattern = r"\{\{\s*([\w.]+)\s*\}\}"
404
+ return set(re.findall(var_pattern, content))
405
+
406
+ def _validate_variable_usage(self, content: str) -> None:
407
+ """
408
+ Validate variable usage in playbook.
409
+
410
+ Args:
411
+ content: Playbook content.
412
+
413
+ """
414
+ # Check for undefined variables (basic check)
415
+ variables = self._extract_jinja2_variables(content)
416
+
417
+ # Check for common issues
418
+ for var in variables:
419
+ if var.startswith("ansible_") and var not in {
420
+ "ansible_facts",
421
+ "ansible_check_mode",
422
+ "ansible_host",
423
+ "ansible_port",
424
+ }:
425
+ self._add_result(
426
+ ValidationLevel.INFO,
427
+ ValidationCategory.SEMANTIC,
428
+ f"Variable '{var}' uses ansible_ prefix",
429
+ suggestion="Verify this is an Ansible built-in variable",
430
+ )
431
+
432
+ def _validate_handler_definitions(self, content: str) -> None:
433
+ """
434
+ Validate handler definitions and usage.
435
+
436
+ Args:
437
+ content: Playbook content.
438
+
439
+ """
440
+ # Check if handlers are referenced but not defined
441
+ notify_pattern = r"notify:\s*([^\n]+)"
442
+ notifies = set(re.findall(notify_pattern, content))
443
+
444
+ if notifies and "handlers:" not in content:
445
+ self._add_result(
446
+ ValidationLevel.WARNING,
447
+ ValidationCategory.SEMANTIC,
448
+ "Tasks reference handlers but no handlers section found",
449
+ suggestion="Add handlers section or remove notify directives",
450
+ )
451
+
452
+ def _validate_playbook_structure(self, content: str) -> None:
453
+ """
454
+ Validate playbook structure and organization.
455
+
456
+ Args:
457
+ content: Playbook content.
458
+
459
+ """
460
+ # Check for required playbook elements
461
+ if "hosts:" not in content:
462
+ self._add_result(
463
+ ValidationLevel.ERROR,
464
+ ValidationCategory.SYNTAX,
465
+ "Playbook missing 'hosts' directive",
466
+ suggestion="Add hosts directive to specify target hosts",
467
+ )
468
+
469
+ if "tasks:" not in content and "roles:" not in content:
470
+ self._add_result(
471
+ ValidationLevel.WARNING,
472
+ ValidationCategory.SYNTAX,
473
+ "Playbook has no tasks or roles",
474
+ suggestion="Add tasks or roles to the playbook",
475
+ )
476
+
477
+ def _validate_jinja2_syntax(self, template: str) -> None:
478
+ """
479
+ Validate Jinja2 template syntax.
480
+
481
+ Args:
482
+ template: Jinja2 template content.
483
+
484
+ """
485
+ try:
486
+ from jinja2 import Environment
487
+
488
+ env = Environment(autoescape=True)
489
+ env.parse(template)
490
+ except Exception as e:
491
+ self._add_result(
492
+ ValidationLevel.ERROR,
493
+ ValidationCategory.SYNTAX,
494
+ f"Invalid Jinja2 syntax: {e}",
495
+ suggestion="Check template syntax and variable references",
496
+ )
497
+
498
+ def _validate_variable_references(self, template: str) -> None:
499
+ """
500
+ Validate variable references in template.
501
+
502
+ Args:
503
+ template: Template content.
504
+
505
+ """
506
+ # Check for undefined variable patterns
507
+ variables = self._extract_jinja2_variables(template)
508
+
509
+ # Check for potential issues
510
+ for var in variables:
511
+ if "." in var:
512
+ parts = var.split(".")
513
+ if len(parts) > 5:
514
+ self._add_result(
515
+ ValidationLevel.INFO,
516
+ ValidationCategory.BEST_PRACTICE,
517
+ f"Deep variable nesting: {var}",
518
+ suggestion="Consider flattening variable structure",
519
+ )
520
+
521
+ def _validate_python_syntax(self, code: str) -> None:
522
+ """
523
+ Validate Python code syntax.
524
+
525
+ Args:
526
+ code: Python code to validate.
527
+
528
+ """
529
+ try:
530
+ ast.parse(code)
531
+ except SyntaxError as e:
532
+ self._add_result(
533
+ ValidationLevel.ERROR,
534
+ ValidationCategory.SYNTAX,
535
+ f"Invalid Python syntax: {e}",
536
+ suggestion="Check Python code syntax",
537
+ )
538
+
539
+ def get_summary(self) -> dict[str, int]:
540
+ """
541
+ Get summary of validation results.
542
+
543
+ Returns:
544
+ Dictionary with counts by level.
545
+
546
+ """
547
+ summary = {"errors": 0, "warnings": 0, "info": 0}
548
+ for result in self.results:
549
+ if result.level == ValidationLevel.ERROR:
550
+ summary["errors"] += 1
551
+ elif result.level == ValidationLevel.WARNING:
552
+ summary["warnings"] += 1
553
+ elif result.level == ValidationLevel.INFO:
554
+ summary["info"] += 1
555
+ return summary