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/cli.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import NoReturn
|
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
|
+
from souschef.converters.playbook import generate_playbook_from_recipe
|
|
14
15
|
from souschef.profiling import (
|
|
15
16
|
generate_cookbook_performance_report,
|
|
16
17
|
profile_function,
|
|
@@ -18,7 +19,10 @@ from souschef.profiling import (
|
|
|
18
19
|
from souschef.server import (
|
|
19
20
|
convert_inspec_to_test,
|
|
20
21
|
convert_resource_to_task,
|
|
22
|
+
generate_github_workflow_from_chef,
|
|
23
|
+
generate_gitlab_ci_from_chef,
|
|
21
24
|
generate_inspec_from_recipe,
|
|
25
|
+
generate_jenkinsfile_from_chef,
|
|
22
26
|
list_cookbook_structure,
|
|
23
27
|
list_directory,
|
|
24
28
|
parse_attributes,
|
|
@@ -30,6 +34,11 @@ from souschef.server import (
|
|
|
30
34
|
read_file,
|
|
31
35
|
)
|
|
32
36
|
|
|
37
|
+
# CI/CD job description constants
|
|
38
|
+
CI_JOB_LINT = " • Lint (cookstyle/foodcritic)"
|
|
39
|
+
CI_JOB_UNIT_TESTS = " • Unit Tests (ChefSpec)"
|
|
40
|
+
CI_JOB_INTEGRATION_TESTS = " • Integration Tests (Test Kitchen)"
|
|
41
|
+
|
|
33
42
|
|
|
34
43
|
@click.group()
|
|
35
44
|
@click.version_option(version="0.1.0", prog_name="souschef")
|
|
@@ -272,16 +281,16 @@ def _display_template_summary(template_file: Path) -> None:
|
|
|
272
281
|
@click.option("--dry-run", is_flag=True, help="Show what would be done")
|
|
273
282
|
def cookbook(cookbook_path: str, output: str | None, dry_run: bool) -> None:
|
|
274
283
|
"""
|
|
275
|
-
|
|
284
|
+
Analyse an entire Chef cookbook.
|
|
276
285
|
|
|
277
286
|
COOKBOOK_PATH: Path to the cookbook root directory
|
|
278
287
|
|
|
279
|
-
This command
|
|
288
|
+
This command analyses the cookbook structure, metadata, recipes,
|
|
280
289
|
attributes, templates, and custom resources.
|
|
281
290
|
"""
|
|
282
291
|
cookbook_dir = Path(cookbook_path)
|
|
283
292
|
|
|
284
|
-
click.echo(f"
|
|
293
|
+
click.echo(f"Analysing cookbook: {cookbook_dir.name}")
|
|
285
294
|
click.echo("=" * 50)
|
|
286
295
|
|
|
287
296
|
# Parse metadata
|
|
@@ -350,7 +359,7 @@ def inspec_parse(path: str, output_format: str) -> None:
|
|
|
350
359
|
@click.option(
|
|
351
360
|
"--format",
|
|
352
361
|
"output_format",
|
|
353
|
-
type=click.Choice(["testinfra", "ansible_assert"]),
|
|
362
|
+
type=click.Choice(["testinfra", "ansible_assert", "serverspec", "goss"]),
|
|
354
363
|
default="testinfra",
|
|
355
364
|
help="Output format for converted tests",
|
|
356
365
|
)
|
|
@@ -382,6 +391,241 @@ def inspec_generate(path: str, output_format: str) -> None:
|
|
|
382
391
|
_output_result(result, output_format)
|
|
383
392
|
|
|
384
393
|
|
|
394
|
+
@cli.command()
|
|
395
|
+
@click.argument("cookbook_path", type=click.Path(exists=True))
|
|
396
|
+
@click.option(
|
|
397
|
+
"--output",
|
|
398
|
+
"-o",
|
|
399
|
+
type=click.Path(),
|
|
400
|
+
help="Output file path for Jenkinsfile (default: ./Jenkinsfile)",
|
|
401
|
+
)
|
|
402
|
+
@click.option(
|
|
403
|
+
"--pipeline-type",
|
|
404
|
+
type=click.Choice(["declarative", "scripted"]),
|
|
405
|
+
default="declarative",
|
|
406
|
+
help="Jenkins pipeline type (default: declarative)",
|
|
407
|
+
)
|
|
408
|
+
@click.option(
|
|
409
|
+
"--parallel/--no-parallel",
|
|
410
|
+
default=True,
|
|
411
|
+
help="Enable parallel test execution (default: enabled)",
|
|
412
|
+
)
|
|
413
|
+
def generate_jenkinsfile(
|
|
414
|
+
cookbook_path: str, output: str | None, pipeline_type: str, parallel: bool
|
|
415
|
+
) -> None:
|
|
416
|
+
"""
|
|
417
|
+
Generate Jenkinsfile for Chef cookbook CI/CD.
|
|
418
|
+
|
|
419
|
+
COOKBOOK_PATH: Path to the Chef cookbook root directory
|
|
420
|
+
|
|
421
|
+
This command analyses the cookbook for CI patterns (Test Kitchen,
|
|
422
|
+
lint tools, test suites) and generates an appropriate Jenkinsfile
|
|
423
|
+
with stages for linting, testing, and convergence.
|
|
424
|
+
|
|
425
|
+
Examples:
|
|
426
|
+
souschef generate-jenkinsfile ./mycookbook
|
|
427
|
+
|
|
428
|
+
souschef generate-jenkinsfile ./mycookbook -o Jenkinsfile.new
|
|
429
|
+
|
|
430
|
+
souschef generate-jenkinsfile ./mycookbook --pipeline-type scripted
|
|
431
|
+
|
|
432
|
+
souschef generate-jenkinsfile ./mycookbook --no-parallel
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
result = generate_jenkinsfile_from_chef(
|
|
437
|
+
cookbook_path=cookbook_path,
|
|
438
|
+
pipeline_type=pipeline_type,
|
|
439
|
+
enable_parallel="yes" if parallel else "no",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Determine output path
|
|
443
|
+
output_path = Path(output) if output else Path.cwd() / "Jenkinsfile"
|
|
444
|
+
|
|
445
|
+
# Write Jenkinsfile
|
|
446
|
+
output_path.write_text(result)
|
|
447
|
+
click.echo(f"✓ Generated {pipeline_type} Jenkinsfile: {output_path}")
|
|
448
|
+
|
|
449
|
+
# Show summary
|
|
450
|
+
click.echo("\nGenerated Pipeline Stages:")
|
|
451
|
+
if "stage('Lint')" in result or "stage 'Lint'" in result:
|
|
452
|
+
click.echo(CI_JOB_LINT)
|
|
453
|
+
if "stage('Unit Tests')" in result or "stage 'Unit Tests'" in result:
|
|
454
|
+
click.echo(CI_JOB_UNIT_TESTS)
|
|
455
|
+
integration_stage = (
|
|
456
|
+
"stage('Integration Tests')" in result
|
|
457
|
+
or "stage 'Integration Tests'" in result
|
|
458
|
+
)
|
|
459
|
+
if integration_stage:
|
|
460
|
+
click.echo(CI_JOB_INTEGRATION_TESTS)
|
|
461
|
+
|
|
462
|
+
if parallel:
|
|
463
|
+
click.echo("\nParallel execution: Enabled")
|
|
464
|
+
else:
|
|
465
|
+
click.echo("\nParallel execution: Disabled")
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
click.echo(f"Error generating Jenkinsfile: {e}", err=True)
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@cli.command()
|
|
473
|
+
@click.argument("cookbook_path", type=click.Path(exists=True))
|
|
474
|
+
@click.option(
|
|
475
|
+
"--output",
|
|
476
|
+
"-o",
|
|
477
|
+
type=click.Path(),
|
|
478
|
+
help="Output file path for .gitlab-ci.yml (default: ./.gitlab-ci.yml)",
|
|
479
|
+
)
|
|
480
|
+
@click.option(
|
|
481
|
+
"--cache/--no-cache",
|
|
482
|
+
default=True,
|
|
483
|
+
help="Enable dependency caching (default: enabled)",
|
|
484
|
+
)
|
|
485
|
+
@click.option(
|
|
486
|
+
"--artifacts/--no-artifacts",
|
|
487
|
+
default=True,
|
|
488
|
+
help="Enable test report artifacts (default: enabled)",
|
|
489
|
+
)
|
|
490
|
+
def generate_gitlab_ci(
|
|
491
|
+
cookbook_path: str, output: str | None, cache: bool, artifacts: bool
|
|
492
|
+
) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Generate .gitlab-ci.yml for Chef cookbook CI/CD.
|
|
495
|
+
|
|
496
|
+
COOKBOOK_PATH: Path to the Chef cookbook root directory
|
|
497
|
+
|
|
498
|
+
This command analyses the cookbook for CI patterns (Test Kitchen,
|
|
499
|
+
lint tools, test suites) and generates an appropriate GitLab CI
|
|
500
|
+
configuration with jobs for linting, testing, and convergence.
|
|
501
|
+
|
|
502
|
+
Examples:
|
|
503
|
+
souschef generate-gitlab-ci ./mycookbook
|
|
504
|
+
|
|
505
|
+
souschef generate-gitlab-ci ./mycookbook -o .gitlab-ci.test.yml
|
|
506
|
+
|
|
507
|
+
souschef generate-gitlab-ci ./mycookbook --no-cache
|
|
508
|
+
|
|
509
|
+
souschef generate-gitlab-ci ./mycookbook --no-artifacts
|
|
510
|
+
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
result = generate_gitlab_ci_from_chef(
|
|
514
|
+
cookbook_path=cookbook_path,
|
|
515
|
+
enable_cache="yes" if cache else "no",
|
|
516
|
+
enable_artifacts="yes" if artifacts else "no",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Determine output path
|
|
520
|
+
output_path = Path(output) if output else Path.cwd() / ".gitlab-ci.yml"
|
|
521
|
+
|
|
522
|
+
# Write GitLab CI config
|
|
523
|
+
output_path.write_text(result)
|
|
524
|
+
click.echo(f"✓ Generated GitLab CI configuration: {output_path}")
|
|
525
|
+
|
|
526
|
+
# Show summary
|
|
527
|
+
click.echo("\nGenerated CI Jobs:")
|
|
528
|
+
if "cookstyle:" in result or "foodcritic:" in result:
|
|
529
|
+
click.echo(CI_JOB_LINT)
|
|
530
|
+
if "unit-test:" in result or "chefspec:" in result:
|
|
531
|
+
click.echo(CI_JOB_UNIT_TESTS)
|
|
532
|
+
if "integration-test:" in result or "kitchen-" in result:
|
|
533
|
+
click.echo(CI_JOB_INTEGRATION_TESTS)
|
|
534
|
+
|
|
535
|
+
click.echo(f"\nCache: {'Enabled' if cache else 'Disabled'}")
|
|
536
|
+
click.echo(f"Artifacts: {'Enabled' if artifacts else 'Disabled'}")
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
click.echo(f"Error generating GitLab CI configuration: {e}", err=True)
|
|
540
|
+
sys.exit(1)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@cli.command()
|
|
544
|
+
@click.argument("cookbook_path", type=click.Path(exists=True))
|
|
545
|
+
@click.option(
|
|
546
|
+
"--output",
|
|
547
|
+
"-o",
|
|
548
|
+
type=click.Path(),
|
|
549
|
+
help="Output file path for workflow (default: ./.github/workflows/ci.yml)",
|
|
550
|
+
)
|
|
551
|
+
@click.option(
|
|
552
|
+
"--workflow-name",
|
|
553
|
+
default="Chef Cookbook CI",
|
|
554
|
+
help="GitHub Actions workflow name (default: Chef Cookbook CI)",
|
|
555
|
+
)
|
|
556
|
+
@click.option(
|
|
557
|
+
"--cache/--no-cache",
|
|
558
|
+
default=True,
|
|
559
|
+
help="Enable dependency caching (default: enabled)",
|
|
560
|
+
)
|
|
561
|
+
@click.option(
|
|
562
|
+
"--artifacts/--no-artifacts",
|
|
563
|
+
default=True,
|
|
564
|
+
help="Enable test report artifacts (default: enabled)",
|
|
565
|
+
)
|
|
566
|
+
def generate_github_workflow(
|
|
567
|
+
cookbook_path: str,
|
|
568
|
+
output: str | None,
|
|
569
|
+
workflow_name: str,
|
|
570
|
+
cache: bool,
|
|
571
|
+
artifacts: bool,
|
|
572
|
+
) -> None:
|
|
573
|
+
"""
|
|
574
|
+
Generate GitHub Actions workflow for Chef cookbook CI/CD.
|
|
575
|
+
|
|
576
|
+
COOKBOOK_PATH: Path to the Chef cookbook root directory
|
|
577
|
+
|
|
578
|
+
This command analyses the cookbook for CI patterns (Test Kitchen,
|
|
579
|
+
lint tools, test suites) and generates an appropriate GitHub Actions
|
|
580
|
+
workflow with jobs for linting, testing, and convergence.
|
|
581
|
+
|
|
582
|
+
Examples:
|
|
583
|
+
souschef generate-github-workflow ./mycookbook
|
|
584
|
+
|
|
585
|
+
souschef generate-github-workflow ./mycookbook -o .github/workflows/test.yml
|
|
586
|
+
|
|
587
|
+
souschef generate-github-workflow ./mycookbook --no-cache
|
|
588
|
+
|
|
589
|
+
souschef generate-github-workflow ./mycookbook --workflow-name "CI Pipeline"
|
|
590
|
+
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
result = generate_github_workflow_from_chef(
|
|
594
|
+
cookbook_path=cookbook_path,
|
|
595
|
+
workflow_name=workflow_name,
|
|
596
|
+
enable_cache="yes" if cache else "no",
|
|
597
|
+
enable_artifacts="yes" if artifacts else "no",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Determine output path
|
|
601
|
+
if output:
|
|
602
|
+
output_path = Path(output)
|
|
603
|
+
else:
|
|
604
|
+
workflows_dir = Path.cwd() / ".github" / "workflows"
|
|
605
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
606
|
+
output_path = workflows_dir / "ci.yml"
|
|
607
|
+
|
|
608
|
+
# Write workflow file
|
|
609
|
+
output_path.write_text(result)
|
|
610
|
+
click.echo(f"✓ Generated GitHub Actions workflow: {output_path}")
|
|
611
|
+
|
|
612
|
+
# Show summary
|
|
613
|
+
click.echo("\nGenerated Workflow Jobs:")
|
|
614
|
+
if "lint:" in result:
|
|
615
|
+
click.echo(CI_JOB_LINT)
|
|
616
|
+
if "unit-test:" in result:
|
|
617
|
+
click.echo(CI_JOB_UNIT_TESTS)
|
|
618
|
+
if "integration-test:" in result:
|
|
619
|
+
click.echo(CI_JOB_INTEGRATION_TESTS)
|
|
620
|
+
|
|
621
|
+
click.echo(f"\nCache: {'Enabled' if cache else 'Disabled'}")
|
|
622
|
+
click.echo(f"Artifacts: {'Enabled' if artifacts else 'Disabled'}")
|
|
623
|
+
|
|
624
|
+
except Exception as e:
|
|
625
|
+
click.echo(f"Error generating GitHub Actions workflow: {e}", err=True)
|
|
626
|
+
sys.exit(1)
|
|
627
|
+
|
|
628
|
+
|
|
385
629
|
def _output_json_format(result: str) -> None:
|
|
386
630
|
"""Output result as JSON format."""
|
|
387
631
|
try:
|
|
@@ -443,7 +687,7 @@ def profile(cookbook_path: str, output: str | None) -> None:
|
|
|
443
687
|
|
|
444
688
|
COOKBOOK_PATH: Path to the Chef cookbook to profile
|
|
445
689
|
|
|
446
|
-
This command
|
|
690
|
+
This command analyses the performance of parsing all cookbook components
|
|
447
691
|
(recipes, attributes, resources, templates) and provides recommendations
|
|
448
692
|
for optimization.
|
|
449
693
|
"""
|
|
@@ -515,6 +759,362 @@ def profile_operation(operation: str, path: str, detailed: bool) -> None:
|
|
|
515
759
|
sys.exit(1)
|
|
516
760
|
|
|
517
761
|
|
|
762
|
+
@cli.command("convert-recipe")
|
|
763
|
+
@click.option(
|
|
764
|
+
"--cookbook-path",
|
|
765
|
+
required=True,
|
|
766
|
+
type=click.Path(exists=True),
|
|
767
|
+
help="Path to the Chef cookbook directory",
|
|
768
|
+
)
|
|
769
|
+
@click.option(
|
|
770
|
+
"--recipe-name",
|
|
771
|
+
default="default",
|
|
772
|
+
help="Name of the recipe to convert (default: default)",
|
|
773
|
+
)
|
|
774
|
+
@click.option(
|
|
775
|
+
"--output-path",
|
|
776
|
+
required=True,
|
|
777
|
+
type=click.Path(),
|
|
778
|
+
help="Directory where Ansible playbook will be written",
|
|
779
|
+
)
|
|
780
|
+
def convert_recipe(cookbook_path: str, recipe_name: str, output_path: str) -> None:
|
|
781
|
+
r"""
|
|
782
|
+
Convert a Chef recipe to an Ansible playbook.
|
|
783
|
+
|
|
784
|
+
This command converts a Chef recipe to an Ansible playbook and writes
|
|
785
|
+
it to the specified output path. Used by the Terraform provider.
|
|
786
|
+
|
|
787
|
+
Example:
|
|
788
|
+
souschef convert-recipe --cookbook-path /chef/cookbooks/nginx \\
|
|
789
|
+
--recipe-name default \\
|
|
790
|
+
--output-path /ansible/playbooks
|
|
791
|
+
|
|
792
|
+
"""
|
|
793
|
+
try:
|
|
794
|
+
cookbook_dir = Path(cookbook_path)
|
|
795
|
+
output_dir = Path(output_path)
|
|
796
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
|
|
798
|
+
# Check recipe exists
|
|
799
|
+
recipe_file = cookbook_dir / "recipes" / f"{recipe_name}.rb"
|
|
800
|
+
if not recipe_file.exists():
|
|
801
|
+
click.echo(
|
|
802
|
+
f"Error: Recipe {recipe_name}.rb not found in {cookbook_path}/recipes",
|
|
803
|
+
err=True,
|
|
804
|
+
)
|
|
805
|
+
sys.exit(1)
|
|
806
|
+
|
|
807
|
+
# Get cookbook name
|
|
808
|
+
metadata_file = cookbook_dir / "metadata.rb"
|
|
809
|
+
cookbook_name = cookbook_dir.name # Default to directory name
|
|
810
|
+
|
|
811
|
+
if metadata_file.exists():
|
|
812
|
+
metadata_result = read_cookbook_metadata(str(metadata_file))
|
|
813
|
+
# Try to parse cookbook name from metadata
|
|
814
|
+
for line in metadata_result.split("\n"):
|
|
815
|
+
if line.startswith("name"):
|
|
816
|
+
cookbook_name = line.split(":", 1)[1].strip()
|
|
817
|
+
break
|
|
818
|
+
|
|
819
|
+
# Generate playbook
|
|
820
|
+
click.echo(f"Converting {cookbook_name}::{recipe_name} to Ansible...")
|
|
821
|
+
playbook_yaml = generate_playbook_from_recipe(str(recipe_file))
|
|
822
|
+
|
|
823
|
+
# Write output
|
|
824
|
+
output_file = output_dir / f"{recipe_name}.yml"
|
|
825
|
+
output_file.write_text(playbook_yaml)
|
|
826
|
+
|
|
827
|
+
click.echo(f"✓ Playbook written to: {output_file}")
|
|
828
|
+
click.echo(f" Size: {len(playbook_yaml)} bytes")
|
|
829
|
+
|
|
830
|
+
except Exception as e:
|
|
831
|
+
click.echo(f"Error converting recipe: {e}", err=True)
|
|
832
|
+
sys.exit(1)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
@cli.command("assess-cookbook")
|
|
836
|
+
@click.option(
|
|
837
|
+
"--cookbook-path",
|
|
838
|
+
required=True,
|
|
839
|
+
type=click.Path(exists=True),
|
|
840
|
+
help="Path to the Chef cookbook directory",
|
|
841
|
+
)
|
|
842
|
+
@click.option(
|
|
843
|
+
"--format",
|
|
844
|
+
"output_format",
|
|
845
|
+
type=click.Choice(["text", "json"]),
|
|
846
|
+
default="json",
|
|
847
|
+
help="Output format (default: json)",
|
|
848
|
+
)
|
|
849
|
+
def assess_cookbook(cookbook_path: str, output_format: str) -> None:
|
|
850
|
+
"""
|
|
851
|
+
Assess a Chef cookbook for migration complexity.
|
|
852
|
+
|
|
853
|
+
Analyses the cookbook and provides complexity level, recipe/resource counts,
|
|
854
|
+
estimated migration effort, and recommendations. Used by Terraform provider.
|
|
855
|
+
|
|
856
|
+
Example:
|
|
857
|
+
souschef assess-cookbook --cookbook-path /chef/cookbooks/nginx --format json
|
|
858
|
+
|
|
859
|
+
"""
|
|
860
|
+
try:
|
|
861
|
+
cookbook_dir = Path(cookbook_path)
|
|
862
|
+
if not cookbook_dir.exists():
|
|
863
|
+
click.echo(
|
|
864
|
+
f"Error: Cookbook path does not exist: {cookbook_path}",
|
|
865
|
+
err=True,
|
|
866
|
+
)
|
|
867
|
+
sys.exit(1)
|
|
868
|
+
|
|
869
|
+
if not cookbook_dir.is_dir():
|
|
870
|
+
click.echo(f"Error: {cookbook_path} is not a directory", err=True)
|
|
871
|
+
sys.exit(1)
|
|
872
|
+
|
|
873
|
+
# Analyse cookbook
|
|
874
|
+
analysis = _analyse_cookbook_for_assessment(cookbook_dir)
|
|
875
|
+
|
|
876
|
+
if output_format == "json":
|
|
877
|
+
click.echo(json.dumps(analysis))
|
|
878
|
+
else:
|
|
879
|
+
_display_assessment_text(cookbook_dir.name, analysis)
|
|
880
|
+
|
|
881
|
+
except Exception as e:
|
|
882
|
+
click.echo(f"Error assessing cookbook: {e}", err=True)
|
|
883
|
+
sys.exit(1)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _analyse_cookbook_for_assessment(cookbook_dir: Path) -> dict:
|
|
887
|
+
"""Analyse cookbook and return assessment data."""
|
|
888
|
+
recipe_count = 0
|
|
889
|
+
resource_count = 0
|
|
890
|
+
recipes_dir = cookbook_dir / "recipes"
|
|
891
|
+
|
|
892
|
+
if recipes_dir.exists():
|
|
893
|
+
recipe_files = list(recipes_dir.glob("*.rb"))
|
|
894
|
+
recipe_count = len(recipe_files)
|
|
895
|
+
for recipe_file in recipe_files:
|
|
896
|
+
content = recipe_file.read_text()
|
|
897
|
+
resource_count += content.count(" do\n") + content.count(" do\r\n")
|
|
898
|
+
|
|
899
|
+
# Determine complexity
|
|
900
|
+
if recipe_count == 0:
|
|
901
|
+
complexity = "Low"
|
|
902
|
+
estimated_hours = 0.5
|
|
903
|
+
elif recipe_count <= 3 and resource_count <= 10:
|
|
904
|
+
complexity = "Low"
|
|
905
|
+
estimated_hours = resource_count * 0.5
|
|
906
|
+
elif recipe_count <= 10 and resource_count <= 50:
|
|
907
|
+
complexity = "Medium"
|
|
908
|
+
estimated_hours = resource_count * 1.0
|
|
909
|
+
else:
|
|
910
|
+
complexity = "High"
|
|
911
|
+
estimated_hours = resource_count * 1.5
|
|
912
|
+
|
|
913
|
+
recommendations = (
|
|
914
|
+
f"Cookbook has {recipe_count} recipes with {resource_count} resources. "
|
|
915
|
+
)
|
|
916
|
+
if complexity == "Low":
|
|
917
|
+
recommendations += "Straightforward migration recommended."
|
|
918
|
+
elif complexity == "Medium":
|
|
919
|
+
recommendations += "Moderate effort required. Consider phased approach."
|
|
920
|
+
else:
|
|
921
|
+
recommendations += (
|
|
922
|
+
"Complex migration. Recommend incremental migration strategy."
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
"complexity": complexity,
|
|
927
|
+
"recipe_count": recipe_count,
|
|
928
|
+
"resource_count": resource_count,
|
|
929
|
+
"estimated_hours": estimated_hours,
|
|
930
|
+
"recommendations": recommendations,
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _display_assessment_text(cookbook_name: str, analysis: dict) -> None:
|
|
935
|
+
"""Display assessment in human-readable text format."""
|
|
936
|
+
click.echo(f"\nCookbook: {cookbook_name}")
|
|
937
|
+
click.echo("=" * 50)
|
|
938
|
+
click.echo(f"Complexity: {analysis['complexity']}")
|
|
939
|
+
click.echo(f"Recipe Count: {analysis['recipe_count']}")
|
|
940
|
+
click.echo(f"Resource Count: {analysis['resource_count']}")
|
|
941
|
+
click.echo(f"Estimated Hours: {analysis['estimated_hours']}")
|
|
942
|
+
click.echo(f"\nRecommendations:\n{analysis['recommendations']}")
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
@cli.command("convert-habitat")
|
|
946
|
+
@click.option(
|
|
947
|
+
"--plan-path",
|
|
948
|
+
required=True,
|
|
949
|
+
type=click.Path(exists=True),
|
|
950
|
+
help="Path to the Habitat plan.sh file",
|
|
951
|
+
)
|
|
952
|
+
@click.option(
|
|
953
|
+
"--output-path",
|
|
954
|
+
required=True,
|
|
955
|
+
type=click.Path(),
|
|
956
|
+
help="Directory where Dockerfile will be written",
|
|
957
|
+
)
|
|
958
|
+
@click.option(
|
|
959
|
+
"--base-image",
|
|
960
|
+
default="ubuntu:latest",
|
|
961
|
+
help="Base Docker image to use (default: ubuntu:latest)",
|
|
962
|
+
)
|
|
963
|
+
def convert_habitat(plan_path: str, output_path: str, base_image: str) -> None:
|
|
964
|
+
r"""
|
|
965
|
+
Convert a Chef Habitat plan to a Dockerfile.
|
|
966
|
+
|
|
967
|
+
Analyses the Habitat plan.sh file and generates an equivalent Dockerfile
|
|
968
|
+
for containerised deployment. Used by Terraform provider.
|
|
969
|
+
|
|
970
|
+
Example:
|
|
971
|
+
souschef convert-habitat --plan-path /hab/plans/nginx/plan.sh \
|
|
972
|
+
--output-path /docker/nginx --base-image ubuntu:22.04
|
|
973
|
+
|
|
974
|
+
"""
|
|
975
|
+
try:
|
|
976
|
+
plan_file = Path(plan_path)
|
|
977
|
+
if not plan_file.exists():
|
|
978
|
+
click.echo(f"Error: Plan file does not exist: {plan_path}", err=True)
|
|
979
|
+
sys.exit(1)
|
|
980
|
+
|
|
981
|
+
if not plan_file.is_file():
|
|
982
|
+
click.echo(f"Error: {plan_path} is not a file", err=True)
|
|
983
|
+
sys.exit(1)
|
|
984
|
+
|
|
985
|
+
output_dir = Path(output_path)
|
|
986
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
987
|
+
|
|
988
|
+
# Call server function to convert
|
|
989
|
+
from souschef.server import convert_habitat_to_dockerfile
|
|
990
|
+
|
|
991
|
+
dockerfile_content = convert_habitat_to_dockerfile(str(plan_path), base_image)
|
|
992
|
+
|
|
993
|
+
# Write Dockerfile
|
|
994
|
+
dockerfile_path = output_dir / "Dockerfile"
|
|
995
|
+
dockerfile_path.write_text(dockerfile_content)
|
|
996
|
+
|
|
997
|
+
click.echo(f"Successfully converted Habitat plan to {dockerfile_path}")
|
|
998
|
+
click.echo(f"Dockerfile size: {len(dockerfile_content)} bytes")
|
|
999
|
+
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
click.echo(f"Error converting Habitat plan: {e}", err=True)
|
|
1002
|
+
sys.exit(1)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@cli.command("convert-inspec")
|
|
1006
|
+
@click.option(
|
|
1007
|
+
"--profile-path",
|
|
1008
|
+
required=True,
|
|
1009
|
+
type=click.Path(exists=True),
|
|
1010
|
+
help="Path to the InSpec profile directory",
|
|
1011
|
+
)
|
|
1012
|
+
@click.option(
|
|
1013
|
+
"--output-path",
|
|
1014
|
+
required=True,
|
|
1015
|
+
type=click.Path(),
|
|
1016
|
+
help="Directory where converted tests will be written",
|
|
1017
|
+
)
|
|
1018
|
+
@click.option(
|
|
1019
|
+
"--format",
|
|
1020
|
+
"output_format",
|
|
1021
|
+
type=click.Choice(["testinfra", "serverspec", "goss", "ansible"]),
|
|
1022
|
+
default="testinfra",
|
|
1023
|
+
help="Output test framework format (default: testinfra)",
|
|
1024
|
+
)
|
|
1025
|
+
def convert_inspec(profile_path: str, output_path: str, output_format: str) -> None:
|
|
1026
|
+
r"""
|
|
1027
|
+
Convert a Chef InSpec profile to various test frameworks.
|
|
1028
|
+
|
|
1029
|
+
Analyses the InSpec profile and generates equivalent tests in the
|
|
1030
|
+
specified framework. Supports TestInfra, Serverspec, Goss, and Ansible.
|
|
1031
|
+
|
|
1032
|
+
Example:
|
|
1033
|
+
souschef convert-inspec --profile-path /inspec/profiles/linux \
|
|
1034
|
+
--output-path /tests/testinfra --format testinfra
|
|
1035
|
+
|
|
1036
|
+
"""
|
|
1037
|
+
try:
|
|
1038
|
+
profile_dir = Path(profile_path)
|
|
1039
|
+
if not profile_dir.exists():
|
|
1040
|
+
click.echo(
|
|
1041
|
+
f"Error: Profile path does not exist: {profile_path}",
|
|
1042
|
+
err=True,
|
|
1043
|
+
)
|
|
1044
|
+
sys.exit(1)
|
|
1045
|
+
|
|
1046
|
+
if not profile_dir.is_dir():
|
|
1047
|
+
click.echo(f"Error: {profile_path} is not a directory", err=True)
|
|
1048
|
+
sys.exit(1)
|
|
1049
|
+
|
|
1050
|
+
output_dir = Path(output_path)
|
|
1051
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1052
|
+
|
|
1053
|
+
# Call server function to convert
|
|
1054
|
+
from souschef.server import convert_inspec_to_test
|
|
1055
|
+
|
|
1056
|
+
test_content = convert_inspec_to_test(str(profile_path), output_format)
|
|
1057
|
+
|
|
1058
|
+
# Determine output filename based on format
|
|
1059
|
+
filename_map = {
|
|
1060
|
+
"testinfra": "test_spec.py",
|
|
1061
|
+
"serverspec": "spec_helper.rb",
|
|
1062
|
+
"goss": "goss.yaml",
|
|
1063
|
+
"ansible": "assert.yml",
|
|
1064
|
+
}
|
|
1065
|
+
output_filename = filename_map.get(output_format, "test.txt")
|
|
1066
|
+
|
|
1067
|
+
# Write test file
|
|
1068
|
+
test_file_path = output_dir / output_filename
|
|
1069
|
+
test_file_path.write_text(test_content)
|
|
1070
|
+
|
|
1071
|
+
click.echo(f"Successfully converted InSpec profile to {output_format} format")
|
|
1072
|
+
click.echo(f"Test file: {test_file_path}")
|
|
1073
|
+
click.echo(f"File size: {len(test_content)} bytes")
|
|
1074
|
+
|
|
1075
|
+
except Exception as e:
|
|
1076
|
+
click.echo(f"Error converting InSpec profile: {e}", err=True)
|
|
1077
|
+
sys.exit(1)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
@cli.command()
|
|
1081
|
+
@click.option("--port", default=8501, help="Port to run the Streamlit app on")
|
|
1082
|
+
def ui(port: int) -> None:
|
|
1083
|
+
"""
|
|
1084
|
+
Launch the SousChef Visual Migration Planning Interface.
|
|
1085
|
+
|
|
1086
|
+
Opens a web-based interface for interactive Chef to Ansible migration planning,
|
|
1087
|
+
cookbook analysis, and visualization.
|
|
1088
|
+
"""
|
|
1089
|
+
try:
|
|
1090
|
+
import subprocess
|
|
1091
|
+
import sys
|
|
1092
|
+
|
|
1093
|
+
# Launch Streamlit app
|
|
1094
|
+
cmd = [
|
|
1095
|
+
sys.executable,
|
|
1096
|
+
"-m",
|
|
1097
|
+
"streamlit",
|
|
1098
|
+
"run",
|
|
1099
|
+
"souschef/ui/app.py",
|
|
1100
|
+
"--server.port",
|
|
1101
|
+
str(port),
|
|
1102
|
+
]
|
|
1103
|
+
click.echo(f"Starting SousChef UI on http://localhost:{port}")
|
|
1104
|
+
click.echo("Press Ctrl+C to stop the server")
|
|
1105
|
+
|
|
1106
|
+
subprocess.run(cmd, check=True)
|
|
1107
|
+
|
|
1108
|
+
except subprocess.CalledProcessError as e:
|
|
1109
|
+
click.echo(f"Error starting UI: {e}", err=True)
|
|
1110
|
+
sys.exit(1)
|
|
1111
|
+
except ImportError:
|
|
1112
|
+
click.echo(
|
|
1113
|
+
"Streamlit is not installed. Install with: pip install streamlit", err=True
|
|
1114
|
+
)
|
|
1115
|
+
sys.exit(1)
|
|
1116
|
+
|
|
1117
|
+
|
|
518
1118
|
def main() -> NoReturn:
|
|
519
1119
|
"""Run the CLI."""
|
|
520
1120
|
cli()
|
souschef/converters/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ from souschef.converters.habitat import (
|
|
|
5
5
|
generate_compose_from_habitat,
|
|
6
6
|
)
|
|
7
7
|
from souschef.converters.playbook import (
|
|
8
|
-
|
|
8
|
+
analyse_chef_search_patterns,
|
|
9
9
|
convert_chef_search_to_inventory,
|
|
10
10
|
generate_dynamic_inventory_script,
|
|
11
11
|
generate_playbook_from_recipe,
|
|
@@ -17,7 +17,7 @@ __all__ = [
|
|
|
17
17
|
"generate_playbook_from_recipe",
|
|
18
18
|
"convert_chef_search_to_inventory",
|
|
19
19
|
"generate_dynamic_inventory_script",
|
|
20
|
-
"
|
|
20
|
+
"analyse_chef_search_patterns",
|
|
21
21
|
"convert_habitat_to_dockerfile",
|
|
22
22
|
"generate_compose_from_habitat",
|
|
23
23
|
]
|