From efb3cfe08de09b9fb6375839f9ac6326c16a0bbe Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 00:02:22 +0500 Subject: [PATCH 1/5] feat: Add installation templates system for common development stacks --- cortex/cli.py | 372 ++++++++++++++++++++++- cortex/templates.py | 518 +++++++++++++++++++++++++++++++ cortex/templates/README.md | 65 ++++ cortex/templates/devops.yaml | 93 ++++++ cortex/templates/lamp.yaml | 68 +++++ cortex/templates/mean.yaml | 64 ++++ cortex/templates/mern.yaml | 64 ++++ cortex/templates/ml-ai.yaml | 66 ++++ docs/TEMPLATES.md | 570 +++++++++++++++++++++++++++++++++++ src/requirements.txt | 1 + test/test_templates.py | 451 +++++++++++++++++++++++++++ 11 files changed, 2322 insertions(+), 10 deletions(-) create mode 100644 cortex/templates.py create mode 100644 cortex/templates/README.md create mode 100644 cortex/templates/devops.yaml create mode 100644 cortex/templates/lamp.yaml create mode 100644 cortex/templates/mean.yaml create mode 100644 cortex/templates/mern.yaml create mode 100644 cortex/templates/ml-ai.yaml create mode 100644 docs/TEMPLATES.md create mode 100644 test/test_templates.py diff --git a/cortex/cli.py b/cortex/cli.py index cdb6044..d9fe5bd 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -10,6 +10,7 @@ from LLM.interpreter import CommandInterpreter from cortex.coordinator import InstallationCoordinator, StepStatus +from cortex.templates import TemplateManager, Template, TemplateFormat, InstallationStep from installation_history import ( InstallationHistory, InstallationType, @@ -55,19 +56,24 @@ def _clear_line(self): sys.stdout.write('\r\033[K') sys.stdout.flush() - def install(self, software: str, execute: bool = False, dry_run: bool = False): - api_key = self._get_api_key() - if not api_key: - return 1 - - provider = self._get_provider() - + def install(self, software: str, execute: bool = False, dry_run: bool = False, template: Optional[str] = None): # Initialize installation history history = InstallationHistory() install_id = None start_time = datetime.now() try: + # If template is specified, use template system + if template: + return self._install_from_template(template, execute, dry_run) + + # Otherwise, use LLM-based installation + api_key = self._get_api_key() + if not api_key: + return 1 + + provider = self._get_provider() + self._print_status("🧠", "Understanding request...") interpreter = CommandInterpreter(api_key=api_key, provider=provider) @@ -261,6 +267,306 @@ def rollback(self, install_id: str, dry_run: bool = False): except Exception as e: self._print_error(f"Rollback failed: {str(e)}") return 1 + + def _install_from_template(self, template_name: str, execute: bool, dry_run: bool): + """Install from a template.""" + history = InstallationHistory() + install_id = None + start_time = datetime.now() + + try: + template_manager = TemplateManager() + + self._print_status("[*]", f"Loading template: {template_name}...") + template = template_manager.load_template(template_name) + + if not template: + self._print_error(f"Template '{template_name}' not found") + self._print_status("[*]", "Available templates:") + templates = template_manager.list_templates() + for name, info in templates.items(): + print(f" - {name}: {info['description']}") + return 1 + + # Display template info + print(f"\n{template.name} Template:") + print(f" {template.description}") + print(f"\n Packages:") + for pkg in template.packages: + print(f" - {pkg}") + + # Check hardware compatibility + is_compatible, warnings = template_manager.check_hardware_compatibility(template) + if warnings: + print(f"\n[WARNING] Hardware Compatibility Warnings:") + for warning in warnings: + print(f" - {warning}") + if not is_compatible and not dry_run: + try: + response = input("\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): ") + if response.lower() != 'y': + return 1 + except (EOFError, KeyboardInterrupt): + # Non-interactive environment or user cancelled + print("\n[INFO] Skipping hardware check prompt (non-interactive mode)") + return 1 + + # Generate commands + self._print_status("[*]", "Generating installation commands...") + commands = template_manager.generate_commands(template) + + if not commands: + self._print_error("No commands generated from template") + return 1 + + # Extract packages for tracking + packages = template.packages if template.packages else history._extract_packages_from_commands(commands) + + # Record installation start + if execute or dry_run: + install_id = history.record_installation( + InstallationType.INSTALL, + packages, + commands, + start_time + ) + + print(f"\n[*] Installing {len(packages)} packages...") + print("\nGenerated commands:") + for i, cmd in enumerate(commands, 1): + print(f" {i}. {cmd}") + + if dry_run: + print("\n(Dry run mode - commands not executed)") + if install_id: + history.update_installation(install_id, InstallationStatus.SUCCESS) + return 0 + + if execute: + # Convert template steps to coordinator format if available + if template.steps: + plan = [ + { + "command": step.command, + "description": step.description, + "rollback": step.rollback + } + for step in template.steps + ] + coordinator = InstallationCoordinator.from_plan( + plan, + timeout=300, + stop_on_error=True + ) + else: + def progress_callback(current, total, step): + status_emoji = "ā³" + if step.status == StepStatus.SUCCESS: + status_emoji = "āœ…" + elif step.status == StepStatus.FAILED: + status_emoji = "āŒ" + print(f"\n[{current}/{total}] {status_emoji} {step.description}") + print(f" Command: {step.command}") + + coordinator = InstallationCoordinator( + commands=commands, + descriptions=[f"Step {i+1}" for i in range(len(commands))], + timeout=300, + stop_on_error=True, + progress_callback=progress_callback + ) + + print("\nExecuting commands...") + result = coordinator.execute() + + if result.success: + # Run verification commands if available + if template.verification_commands: + self._print_status("[*]", "Verifying installation...") + verify_results = coordinator.verify_installation(template.verification_commands) + all_passed = all(verify_results.values()) + if not all_passed: + print("\n[WARNING] Some verification checks failed:") + for cmd, passed in verify_results.items(): + status = "[OK]" if passed else "[FAIL]" + print(f" {status} {cmd}") + + # Run post-install commands + if template.post_install: + self._print_status("[*]", "Running post-installation steps...") + for cmd in template.post_install: + subprocess.run(cmd, shell=True) + + self._print_success(f"{template.name} stack ready!") + print(f"\nCompleted in {result.total_duration:.2f} seconds") + + # Display post-install info + if template.post_install: + print("\n[*] Post-installation information:") + for cmd in template.post_install: + if cmd.startswith("echo"): + subprocess.run(cmd, shell=True) + + # Record successful installation + if install_id: + history.update_installation(install_id, InstallationStatus.SUCCESS) + print(f"\n[*] Installation recorded (ID: {install_id})") + print(f" To rollback: cortex rollback {install_id}") + + return 0 + else: + # Record failed installation + if install_id: + error_msg = result.error_message or "Installation failed" + history.update_installation( + install_id, + InstallationStatus.FAILED, + error_msg + ) + + if result.failed_step is not None: + self._print_error(f"Installation failed at step {result.failed_step + 1}") + else: + self._print_error("Installation failed") + if result.error_message: + print(f" Error: {result.error_message}", file=sys.stderr) + if install_id: + print(f"\nšŸ“ Installation recorded (ID: {install_id})") + print(f" View details: cortex history show {install_id}") + return 1 + else: + print("\nTo execute these commands, run with --execute flag") + print(f"Example: cortex install --template {template_name} --execute") + + return 0 + + except ValueError as e: + if install_id: + history.update_installation(install_id, InstallationStatus.FAILED, str(e)) + self._print_error(str(e)) + return 1 + except Exception as e: + if install_id: + history.update_installation(install_id, InstallationStatus.FAILED, str(e)) + self._print_error(f"Unexpected error: {str(e)}") + return 1 + + def template_list(self): + """List all available templates.""" + try: + template_manager = TemplateManager() + templates = template_manager.list_templates() + + if not templates: + print("No templates found.") + return 0 + + print("\nAvailable Templates:") + print("=" * 80) + print(f"{'Name':<20} {'Version':<12} {'Type':<12} {'Description':<35}") + print("=" * 80) + + for name, info in sorted(templates.items()): + desc = info['description'][:33] + "..." if len(info['description']) > 35 else info['description'] + print(f"{name:<20} {info['version']:<12} {info['type']:<12} {desc:<35}") + + print(f"\nTotal: {len(templates)} templates") + return 0 + except Exception as e: + self._print_error(f"Failed to list templates: {str(e)}") + return 1 + + def template_create(self, name: str, interactive: bool = True): + """Create a new template interactively.""" + try: + print(f"\n[*] Creating template: {name}") + + if interactive: + description = input("Description: ").strip() + if not description: + self._print_error("Description is required") + return 1 + + version = input("Version (default: 1.0.0): ").strip() or "1.0.0" + author = input("Author (optional): ").strip() or None + + print("\nEnter packages (one per line, empty line to finish):") + packages = [] + while True: + pkg = input(" Package: ").strip() + if not pkg: + break + packages.append(pkg) + + # Create template + from cortex.templates import Template, HardwareRequirements + template = Template( + name=name, + description=description, + version=version, + author=author, + packages=packages + ) + + # Ask about hardware requirements + print("\nHardware Requirements (optional):") + min_ram = input(" Minimum RAM (MB, optional): ").strip() + min_cores = input(" Minimum CPU cores (optional): ").strip() + min_storage = input(" Minimum storage (MB, optional): ").strip() + + if min_ram or min_cores or min_storage: + hw_req = HardwareRequirements( + min_ram_mb=int(min_ram) if min_ram else None, + min_cores=int(min_cores) if min_cores else None, + min_storage_mb=int(min_storage) if min_storage else None + ) + template.hardware_requirements = hw_req + + # Save template + template_manager = TemplateManager() + template_path = template_manager.save_template(template, name) + + self._print_success(f"Template '{name}' created successfully!") + print(f" Saved to: {template_path}") + return 0 + else: + self._print_error("Non-interactive template creation not yet supported") + return 1 + + except Exception as e: + self._print_error(f"Failed to create template: {str(e)}") + return 1 + + def template_import(self, file_path: str, name: Optional[str] = None): + """Import a template from a file.""" + try: + template_manager = TemplateManager() + template = template_manager.import_template(file_path, name) + + # Save to user templates + save_name = name or template.name + template_path = template_manager.save_template(template, save_name) + + self._print_success(f"Template '{save_name}' imported successfully!") + print(f" Saved to: {template_path}") + return 0 + except Exception as e: + self._print_error(f"Failed to import template: {str(e)}") + return 1 + + def template_export(self, name: str, file_path: str, format: str = "yaml"): + """Export a template to a file.""" + try: + template_manager = TemplateManager() + template_format = TemplateFormat.YAML if format.lower() == "yaml" else TemplateFormat.JSON + export_path = template_manager.export_template(name, file_path, template_format) + + self._print_success(f"Template '{name}' exported successfully!") + print(f" Saved to: {export_path}") + return 0 + except Exception as e: + self._print_error(f"Failed to export template: {str(e)}") + return 1 def main(): @@ -274,6 +580,11 @@ def main(): cortex install docker --execute cortex install "python 3.11 with pip" cortex install nginx --dry-run + cortex install --template lamp --execute + cortex template list + cortex template create my-stack + cortex template import template.yaml + cortex template export lamp my-template.yaml cortex history cortex history show cortex rollback @@ -287,8 +598,9 @@ def main(): subparsers = parser.add_subparsers(dest='command', help='Available commands') # Install command - install_parser = subparsers.add_parser('install', help='Install software using natural language') - install_parser.add_argument('software', type=str, help='Software to install (natural language)') + install_parser = subparsers.add_parser('install', help='Install software using natural language or template') + install_parser.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') + install_parser.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') install_parser.add_argument('--execute', action='store_true', help='Execute the generated commands') install_parser.add_argument('--dry-run', action='store_true', help='Show commands without executing') @@ -304,6 +616,28 @@ def main(): rollback_parser.add_argument('id', help='Installation ID to rollback') rollback_parser.add_argument('--dry-run', action='store_true', help='Show rollback actions without executing') + # Template command + template_parser = subparsers.add_parser('template', help='Manage installation templates') + template_subparsers = template_parser.add_subparsers(dest='template_action', help='Template actions') + + # Template list + template_list_parser = template_subparsers.add_parser('list', help='List all available templates') + + # Template create + template_create_parser = template_subparsers.add_parser('create', help='Create a new template') + template_create_parser.add_argument('name', type=str, help='Template name') + + # Template import + template_import_parser = template_subparsers.add_parser('import', help='Import a template from file') + template_import_parser.add_argument('file_path', type=str, help='Path to template file') + template_import_parser.add_argument('--name', type=str, help='Optional new name for the template') + + # Template export + template_export_parser = template_subparsers.add_parser('export', help='Export a template to file') + template_export_parser.add_argument('name', type=str, help='Template name to export') + template_export_parser.add_argument('file_path', type=str, help='Destination file path') + template_export_parser.add_argument('--format', choices=['yaml', 'json'], default='yaml', help='Export format') + args = parser.parse_args() if not args.command: @@ -314,11 +648,29 @@ def main(): try: if args.command == 'install': - return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) + if args.template: + return cli.install("", execute=args.execute, dry_run=args.dry_run, template=args.template) + elif args.software: + return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) + else: + install_parser.print_help() + return 1 elif args.command == 'history': return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) elif args.command == 'rollback': return cli.rollback(args.id, dry_run=args.dry_run) + elif args.command == 'template': + if args.template_action == 'list': + return cli.template_list() + elif args.template_action == 'create': + return cli.template_create(args.name) + elif args.template_action == 'import': + return cli.template_import(args.file_path, args.name) + elif args.template_action == 'export': + return cli.template_export(args.name, args.file_path, args.format) + else: + template_parser.print_help() + return 1 else: parser.print_help() return 1 diff --git a/cortex/templates.py b/cortex/templates.py new file mode 100644 index 0000000..f4dd7f6 --- /dev/null +++ b/cortex/templates.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +""" +Template System for Cortex Linux Installation Templates + +Supports pre-built templates for common development stacks (LAMP, MEAN, MERN, etc.) +and custom template creation, validation, and hardware-aware selection. +""" + +import json +import yaml +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Any, Set, Tuple +from dataclasses import dataclass, field +from enum import Enum + +# Add parent directory to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from src.hwprofiler import HardwareProfiler +from cortex.packages import PackageManager, PackageManagerType + + +class TemplateFormat(Enum): + """Supported template formats.""" + YAML = "yaml" + JSON = "json" + + +@dataclass +class HardwareRequirements: + """Hardware requirements for a template.""" + min_ram_mb: Optional[int] = None + min_cores: Optional[int] = None + min_storage_mb: Optional[int] = None + requires_gpu: bool = False + gpu_vendor: Optional[str] = None # "NVIDIA", "AMD", "Intel" + requires_cuda: bool = False + min_cuda_version: Optional[str] = None + + +@dataclass +class InstallationStep: + """A single installation step in a template.""" + command: str + description: str + rollback: Optional[str] = None + verify: Optional[str] = None + requires_root: bool = True + + +@dataclass +class Template: + """Represents an installation template.""" + name: str + description: str + version: str + author: Optional[str] = None + packages: List[str] = field(default_factory=list) + steps: List[InstallationStep] = field(default_factory=list) + hardware_requirements: Optional[HardwareRequirements] = None + post_install: List[str] = field(default_factory=list) + verification_commands: List[str] = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Convert template to dictionary.""" + result = { + "name": self.name, + "description": self.description, + "version": self.version, + "packages": self.packages, + "post_install": self.post_install, + "verification_commands": self.verification_commands, + "metadata": self.metadata + } + + if self.author: + result["author"] = self.author + + if self.steps: + result["steps"] = [ + { + "command": step.command, + "description": step.description, + "rollback": step.rollback, + "verify": step.verify, + "requires_root": step.requires_root + } + for step in self.steps + ] + + if self.hardware_requirements: + hw = self.hardware_requirements + result["hardware_requirements"] = { + "min_ram_mb": hw.min_ram_mb, + "min_cores": hw.min_cores, + "min_storage_mb": hw.min_storage_mb, + "requires_gpu": hw.requires_gpu, + "gpu_vendor": hw.gpu_vendor, + "requires_cuda": hw.requires_cuda, + "min_cuda_version": hw.min_cuda_version + } + + return result + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Template": + """Create template from dictionary.""" + # Parse hardware requirements + hw_req = None + if "hardware_requirements" in data: + hw_data = data["hardware_requirements"] + hw_req = HardwareRequirements( + min_ram_mb=hw_data.get("min_ram_mb"), + min_cores=hw_data.get("min_cores"), + min_storage_mb=hw_data.get("min_storage_mb"), + requires_gpu=hw_data.get("requires_gpu", False), + gpu_vendor=hw_data.get("gpu_vendor"), + requires_cuda=hw_data.get("requires_cuda", False), + min_cuda_version=hw_data.get("min_cuda_version") + ) + + # Parse installation steps + steps = [] + if "steps" in data: + for step_data in data["steps"]: + steps.append(InstallationStep( + command=step_data["command"], + description=step_data.get("description", ""), + rollback=step_data.get("rollback"), + verify=step_data.get("verify"), + requires_root=step_data.get("requires_root", True) + )) + + return cls( + name=data["name"], + description=data["description"], + version=data.get("version", "1.0.0"), + author=data.get("author"), + packages=data.get("packages", []), + steps=steps, + hardware_requirements=hw_req, + post_install=data.get("post_install", []), + verification_commands=data.get("verification_commands", []), + metadata=data.get("metadata", {}) + ) + + +class TemplateValidator: + """Validates template structure and content.""" + + REQUIRED_FIELDS = ["name", "description", "version"] + REQUIRED_STEP_FIELDS = ["command", "description"] + + @staticmethod + def validate(template: Template) -> Tuple[bool, List[str]]: + """ + Validate a template. + + Returns: + Tuple of (is_valid, list_of_errors) + """ + errors = [] + + # Check required fields + if not template.name: + errors.append("Template name is required") + if not template.description: + errors.append("Template description is required") + if not template.version: + errors.append("Template version is required") + + # Validate steps + for i, step in enumerate(template.steps): + if not step.command: + errors.append(f"Step {i+1}: command is required") + if not step.description: + errors.append(f"Step {i+1}: description is required") + + # Validate packages list + if not template.packages and not template.steps: + errors.append("Template must have either packages or steps defined") + + # Validate hardware requirements + if template.hardware_requirements: + hw = template.hardware_requirements + if hw.min_ram_mb is not None and hw.min_ram_mb < 0: + errors.append("min_ram_mb must be non-negative") + if hw.min_cores is not None and hw.min_cores < 0: + errors.append("min_cores must be non-negative") + if hw.min_storage_mb is not None and hw.min_storage_mb < 0: + errors.append("min_storage_mb must be non-negative") + if hw.requires_cuda and not hw.requires_gpu: + errors.append("requires_cuda is true but requires_gpu is false") + + return len(errors) == 0, errors + + +class TemplateManager: + """Manages installation templates.""" + + def __init__(self, templates_dir: Optional[str] = None): + """ + Initialize template manager. + + Args: + templates_dir: Directory containing templates (defaults to built-in templates) + """ + if templates_dir: + self.templates_dir = Path(templates_dir) + else: + # Default to built-in templates directory + base_dir = Path(__file__).parent + self.templates_dir = base_dir / "templates" + + self.user_templates_dir = Path.home() / ".cortex" / "templates" + self.user_templates_dir.mkdir(parents=True, exist_ok=True) + + self._templates_cache: Dict[str, Template] = {} + self._hardware_profiler = HardwareProfiler() + self._package_manager = PackageManager() + + def _get_template_path(self, name: str) -> Optional[Path]: + """Find template file by name.""" + # Check user templates first + for ext in [".yaml", ".yml", ".json"]: + user_path = self.user_templates_dir / f"{name}{ext}" + if user_path.exists(): + return user_path + + # Check built-in templates + for ext in [".yaml", ".yml", ".json"]: + builtin_path = self.templates_dir / f"{name}{ext}" + if builtin_path.exists(): + return builtin_path + + return None + + def load_template(self, name: str) -> Optional[Template]: + """Load a template by name.""" + if name in self._templates_cache: + return self._templates_cache[name] + + template_path = self._get_template_path(name) + if not template_path: + return None + + try: + with open(template_path, 'r', encoding='utf-8') as f: + if template_path.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + + template = Template.from_dict(data) + self._templates_cache[name] = template + return template + except Exception as e: + raise ValueError(f"Failed to load template {name}: {str(e)}") + + def save_template(self, template: Template, name: Optional[str] = None, + format: TemplateFormat = TemplateFormat.YAML) -> Path: + """ + Save a template to user templates directory. + + Args: + template: Template to save + name: Template name (defaults to template.name) + format: File format (YAML or JSON) + + Returns: + Path to saved template file + """ + # Validate template + is_valid, errors = TemplateValidator.validate(template) + if not is_valid: + raise ValueError(f"Template validation failed: {', '.join(errors)}") + + template_name = name or template.name + ext = ".yaml" if format == TemplateFormat.YAML else ".json" + template_path = self.user_templates_dir / f"{template_name}{ext}" + + data = template.to_dict() + + with open(template_path, 'w', encoding='utf-8') as f: + if format == TemplateFormat.YAML: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + else: + json.dump(data, f, indent=2) + + return template_path + + def list_templates(self) -> Dict[str, Dict[str, Any]]: + """List all available templates.""" + templates = {} + + # List built-in templates + if self.templates_dir.exists(): + for ext in [".yaml", ".yml", ".json"]: + for template_file in self.templates_dir.glob(f"*{ext}"): + name = template_file.stem + try: + template = self.load_template(name) + if template: + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "built-in", + "path": str(template_file) + } + except Exception: + pass + + # List user templates + if self.user_templates_dir.exists(): + for ext in [".yaml", ".yml", ".json"]: + for template_file in self.user_templates_dir.glob(f"*{ext}"): + name = template_file.stem + if name not in templates: # Don't override built-in + try: + template = self.load_template(name) + if template: + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "user", + "path": str(template_file) + } + except Exception: + pass + + return templates + + def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[str]]: + """ + Check if current hardware meets template requirements. + + Returns: + Tuple of (is_compatible, list_of_warnings) + """ + if not template.hardware_requirements: + return True, [] + + hw_profile = self._hardware_profiler.profile() + hw_req = template.hardware_requirements + warnings = [] + + # Check RAM + if hw_req.min_ram_mb: + available_ram = hw_profile.get("ram", 0) + if available_ram < hw_req.min_ram_mb: + warnings.append( + f"Insufficient RAM: {available_ram}MB available, " + f"{hw_req.min_ram_mb}MB required" + ) + + # Check CPU cores + if hw_req.min_cores: + available_cores = hw_profile.get("cpu", {}).get("cores", 0) + if available_cores < hw_req.min_cores: + warnings.append( + f"Insufficient CPU cores: {available_cores} available, " + f"{hw_req.min_cores} required" + ) + + # Check storage + if hw_req.min_storage_mb: + total_storage = sum( + s.get("size", 0) for s in hw_profile.get("storage", []) + ) + if total_storage < hw_req.min_storage_mb: + warnings.append( + f"Insufficient storage: {total_storage}MB available, " + f"{hw_req.min_storage_mb}MB required" + ) + + # Check GPU requirements + if hw_req.requires_gpu: + gpus = hw_profile.get("gpu", []) + if not gpus: + warnings.append("GPU required but not detected") + elif hw_req.gpu_vendor: + vendor_match = any( + g.get("vendor") == hw_req.gpu_vendor for g in gpus + ) + if not vendor_match: + warnings.append( + f"{hw_req.gpu_vendor} GPU required but not found" + ) + + # Check CUDA requirements + if hw_req.requires_cuda: + gpus = hw_profile.get("gpu", []) + cuda_found = False + for gpu in gpus: + if gpu.get("vendor") == "NVIDIA" and gpu.get("cuda"): + cuda_version = gpu.get("cuda", "") + if hw_req.min_cuda_version: + # Simple version comparison + try: + gpu_cuda = tuple(map(int, cuda_version.split('.'))) + req_cuda = tuple(map(int, hw_req.min_cuda_version.split('.'))) + if gpu_cuda >= req_cuda: + cuda_found = True + break + except ValueError: + # If version parsing fails, just check if CUDA exists + cuda_found = True + break + else: + cuda_found = True + break + + if not cuda_found: + warnings.append( + f"CUDA {hw_req.min_cuda_version or ''} required but not found" + ) + + is_compatible = len(warnings) == 0 + return is_compatible, warnings + + def generate_commands(self, template: Template) -> List[str]: + """ + Generate installation commands from template. + + Returns: + List of installation commands + """ + commands = [] + + # If template has explicit steps, use those + if template.steps: + commands = [step.command for step in template.steps] + # Otherwise, generate from packages + elif template.packages: + # Use package manager to generate commands + pm = PackageManager() + package_list = " ".join(template.packages) + try: + commands = pm.parse(f"install {package_list}") + except ValueError: + # Fallback: direct apt/yum install + pm_type = pm.pm_type.value + commands = [f"{pm_type} install -y {' '.join(template.packages)}"] + + return commands + + def import_template(self, file_path: str, name: Optional[str] = None) -> Template: + """ + Import a template from a file. + + Args: + file_path: Path to template file + name: Optional new name for the template + + Returns: + Loaded template + """ + template_path = Path(file_path) + if not template_path.exists(): + raise FileNotFoundError(f"Template file not found: {file_path}") + + try: + with open(template_path, 'r', encoding='utf-8') as f: + if template_path.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + + template = Template.from_dict(data) + + # Override name if provided + if name: + template.name = name + + # Validate + is_valid, errors = TemplateValidator.validate(template) + if not is_valid: + raise ValueError(f"Template validation failed: {', '.join(errors)}") + + return template + except Exception as e: + raise ValueError(f"Failed to import template: {str(e)}") + + def export_template(self, name: str, file_path: str, + format: TemplateFormat = TemplateFormat.YAML) -> Path: + """ + Export a template to a file. + + Args: + name: Template name + file_path: Destination file path + format: File format + + Returns: + Path to exported file + """ + template = self.load_template(name) + if not template: + raise ValueError(f"Template not found: {name}") + + export_path = Path(file_path) + data = template.to_dict() + + with open(export_path, 'w', encoding='utf-8') as f: + if format == TemplateFormat.YAML: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + else: + json.dump(data, f, indent=2) + + return export_path + diff --git a/cortex/templates/README.md b/cortex/templates/README.md new file mode 100644 index 0000000..44dd788 --- /dev/null +++ b/cortex/templates/README.md @@ -0,0 +1,65 @@ +# Installation Templates + +This directory contains built-in installation templates for common development stacks. + +## Available Templates + +- **lamp.yaml** - LAMP Stack (Linux, Apache, MySQL, PHP) +- **mean.yaml** - MEAN Stack (MongoDB, Express.js, Angular, Node.js) +- **mern.yaml** - MERN Stack (MongoDB, Express.js, React, Node.js) +- **ml-ai.yaml** - Machine Learning / AI Stack +- **devops.yaml** - DevOps Stack (Docker, Kubernetes, Terraform, etc.) + +## Usage + +```bash +# List all templates +cortex template list + +# Install from template +cortex install --template lamp --execute + +# Create custom template +cortex template create my-stack + +# Import template +cortex template import my-template.yaml + +# Export template +cortex template export lamp my-lamp.yaml +``` + +## Template Format + +Templates are defined in YAML format with the following structure: + +```yaml +name: Template Name +description: Template description +version: 1.0.0 +author: Author Name (optional) + +packages: + - package1 + - package2 + +steps: + - command: apt update + description: Update packages + requires_root: true + rollback: (optional) + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "Installation complete" + +verification_commands: + - package --version +``` + +See [TEMPLATES.md](../../docs/TEMPLATES.md) for complete documentation. + diff --git a/cortex/templates/devops.yaml b/cortex/templates/devops.yaml new file mode 100644 index 0000000..f9c8751 --- /dev/null +++ b/cortex/templates/devops.yaml @@ -0,0 +1,93 @@ +name: DevOps Stack +description: Complete DevOps toolchain with Docker, Kubernetes, Terraform, Ansible, and CI/CD tools +version: 1.0.0 +author: Cortex Linux +packages: + - docker.io + - docker-compose + - kubectl + - git + - curl + - wget + - ansible + - terraform + - jenkins + - gitlab-runner + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y apt-transport-https ca-certificates curl gnupg lsb-release + description: Install prerequisites for Docker + requires_root: true + - command: install -m 0755 -d /etc/apt/keyrings + description: Create keyrings directory + requires_root: true + - command: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + description: Add Docker GPG key + requires_root: true + - command: chmod a+r /etc/apt/keyrings/docker.gpg + description: Set key permissions + requires_root: true + - command: echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + description: Add Docker repository + requires_root: true + - command: apt update + description: Update package lists with Docker repo + requires_root: true + - command: apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + description: Install Docker + requires_root: true + - command: systemctl start docker + description: Start Docker service + requires_root: true + rollback: systemctl stop docker + - command: systemctl enable docker + description: Enable Docker on boot + requires_root: true + rollback: systemctl disable docker + - command: curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + description: Download kubectl + requires_root: false + - command: install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + description: Install kubectl + requires_root: true + - command: rm kubectl + description: Clean up kubectl download + requires_root: false + - command: apt install -y ansible terraform git + description: Install Ansible, Terraform, and Git + requires_root: true + +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 + min_storage_mb: 20480 + +post_install: + - echo "DevOps stack installed successfully" + - echo "Docker version: $(docker --version)" + - echo "Kubernetes: $(kubectl version --client --short 2>/dev/null || echo 'installed')" + - echo "Terraform: $(terraform --version | head -n1)" + - echo "Ansible: $(ansible --version | head -n1)" + +verification_commands: + - docker --version + - docker ps + - kubectl version --client + - terraform --version + - ansible --version + - git --version + - systemctl is-active docker + +metadata: + category: devops + tags: + - docker + - kubernetes + - terraform + - ansible + - ci-cd + - infrastructure + diff --git a/cortex/templates/lamp.yaml b/cortex/templates/lamp.yaml new file mode 100644 index 0000000..5421437 --- /dev/null +++ b/cortex/templates/lamp.yaml @@ -0,0 +1,68 @@ +name: LAMP Stack +description: Linux, Apache, MySQL, PHP stack for web development +version: 1.0.0 +author: Cortex Linux +packages: + - apache2 + - mysql-server + - mysql-client + - php + - php-mysql + - php-mbstring + - php-xml + - php-curl + - phpmyadmin + - libapache2-mod-php + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php + description: Install LAMP stack packages + requires_root: true + - command: apt install -y phpmyadmin + description: Install phpMyAdmin + requires_root: true + - command: systemctl start apache2 + description: Start Apache web server + requires_root: true + rollback: systemctl stop apache2 + - command: systemctl enable apache2 + description: Enable Apache on boot + requires_root: true + rollback: systemctl disable apache2 + - command: systemctl start mysql + description: Start MySQL service + requires_root: true + rollback: systemctl stop mysql + - command: systemctl enable mysql + description: Enable MySQL on boot + requires_root: true + rollback: systemctl disable mysql + +hardware_requirements: + min_ram_mb: 1024 + min_cores: 2 + min_storage_mb: 5120 + +post_install: + - echo "LAMP stack installed successfully" + - echo "Apache: http://localhost" + - echo "phpMyAdmin: http://localhost/phpmyadmin" + +verification_commands: + - apache2 -v + - mysql --version + - php -v + - systemctl is-active apache2 + - systemctl is-active mysql + +metadata: + category: web-development + tags: + - web + - server + - database + - php + diff --git a/cortex/templates/mean.yaml b/cortex/templates/mean.yaml new file mode 100644 index 0000000..21bb9c6 --- /dev/null +++ b/cortex/templates/mean.yaml @@ -0,0 +1,64 @@ +name: MEAN Stack +description: MongoDB, Express.js, Angular, Node.js stack for modern web applications +version: 1.0.0 +author: Cortex Linux +packages: + - nodejs + - npm + - mongodb + - git + - curl + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + description: Add Node.js repository + requires_root: true + - command: apt install -y nodejs + description: Install Node.js + requires_root: true + - command: npm install -g @angular/cli express-generator + description: Install Angular CLI and Express generator globally + requires_root: false + - command: apt install -y mongodb + description: Install MongoDB + requires_root: true + - command: systemctl start mongodb + description: Start MongoDB service + requires_root: true + rollback: systemctl stop mongodb + - command: systemctl enable mongodb + description: Enable MongoDB on boot + requires_root: true + rollback: systemctl disable mongodb + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "MEAN stack installed successfully" + - echo "Node.js version: $(node --version)" + - echo "npm version: $(npm --version)" + - echo "Angular CLI: $(ng version 2>/dev/null || echo 'installed')" + - echo "MongoDB: mongodb://localhost:27017" + +verification_commands: + - node --version + - npm --version + - mongod --version + - systemctl is-active mongodb + - ng version + +metadata: + category: web-development + tags: + - javascript + - nodejs + - mongodb + - angular + - express + diff --git a/cortex/templates/mern.yaml b/cortex/templates/mern.yaml new file mode 100644 index 0000000..953606a --- /dev/null +++ b/cortex/templates/mern.yaml @@ -0,0 +1,64 @@ +name: MERN Stack +description: MongoDB, Express.js, React, Node.js stack for full-stack JavaScript development +version: 1.0.0 +author: Cortex Linux +packages: + - nodejs + - npm + - mongodb + - git + - curl + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + description: Add Node.js repository + requires_root: true + - command: apt install -y nodejs + description: Install Node.js + requires_root: true + - command: npm install -g create-react-app express-generator + description: Install React and Express generators globally + requires_root: false + - command: apt install -y mongodb + description: Install MongoDB + requires_root: true + - command: systemctl start mongodb + description: Start MongoDB service + requires_root: true + rollback: systemctl stop mongodb + - command: systemctl enable mongodb + description: Enable MongoDB on boot + requires_root: true + rollback: systemctl disable mongodb + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "MERN stack installed successfully" + - echo "Node.js version: $(node --version)" + - echo "npm version: $(npm --version)" + - echo "React: Create apps with 'npx create-react-app'" + - echo "MongoDB: mongodb://localhost:27017" + +verification_commands: + - node --version + - npm --version + - mongod --version + - systemctl is-active mongodb + - npx create-react-app --version + +metadata: + category: web-development + tags: + - javascript + - nodejs + - mongodb + - react + - express + diff --git a/cortex/templates/ml-ai.yaml b/cortex/templates/ml-ai.yaml new file mode 100644 index 0000000..5880961 --- /dev/null +++ b/cortex/templates/ml-ai.yaml @@ -0,0 +1,66 @@ +name: ML/AI Stack +description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter +version: 1.0.0 +author: Cortex Linux +packages: + - python3 + - python3-pip + - python3-venv + - python3-dev + - build-essential + - git + - curl + - wget + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y python3 python3-pip python3-venv python3-dev build-essential git curl wget + description: Install Python and build tools + requires_root: true + - command: pip3 install --upgrade pip + description: Upgrade pip to latest version + requires_root: false + - command: pip3 install numpy pandas scipy matplotlib scikit-learn jupyter notebook + description: Install core ML libraries + requires_root: false + - command: pip3 install tensorflow torch torchvision torchaudio + description: Install deep learning frameworks + requires_root: false + - command: pip3 install seaborn plotly opencv-python-headless + description: Install additional ML/visualization libraries + requires_root: false + +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 + min_storage_mb: 20480 + requires_gpu: false + requires_cuda: false + +post_install: + - echo "ML/AI stack installed successfully" + - echo "Python version: $(python3 --version)" + - echo "Start Jupyter: jupyter notebook" + - echo "TensorFlow: $(python3 -c 'import tensorflow as tf; print(tf.__version__)' 2>/dev/null || echo 'installed')" + - echo "PyTorch: $(python3 -c 'import torch; print(torch.__version__)' 2>/dev/null || echo 'installed')" + +verification_commands: + - python3 --version + - pip3 --version + - python3 -c "import numpy; print('NumPy:', numpy.__version__)" + - python3 -c "import pandas; print('Pandas:', pandas.__version__)" + - python3 -c "import sklearn; print('Scikit-learn:', sklearn.__version__)" + - jupyter --version + +metadata: + category: machine-learning + tags: + - python + - machine-learning + - ai + - tensorflow + - pytorch + - jupyter + diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md new file mode 100644 index 0000000..4fc1234 --- /dev/null +++ b/docs/TEMPLATES.md @@ -0,0 +1,570 @@ +# Installation Templates Guide + +Cortex Linux provides a powerful template system for installing common development stacks and software bundles. Templates are pre-configured installation definitions that can be shared, customized, and reused. + +## Overview + +Templates allow you to: +- Install complete development stacks with a single command +- Share installation configurations with your team +- Create custom templates for your specific needs +- Validate hardware compatibility before installation +- Export and import templates for easy sharing + +## Quick Start + +### Installing from a Template + +```bash +# List available templates +cortex template list + +# Install LAMP stack +cortex install --template lamp --execute + +# Install MEAN stack (dry run first) +cortex install --template mean --dry-run +cortex install --template mean --execute +``` + +### Creating a Custom Template + +```bash +# Create a new template interactively +cortex template create my-stack + +# Import a template from file +cortex template import my-template.yaml + +# Export a template +cortex template export lamp my-lamp-template.yaml +``` + +## Built-in Templates + +Cortex Linux comes with 5+ pre-built templates: + +### 1. LAMP Stack + +Linux, Apache, MySQL, PHP stack for traditional web development. + +```bash +cortex install --template lamp --execute +``` + +**Packages:** +- Apache 2.4 +- MySQL 8.0 +- PHP 8.2 +- phpMyAdmin + +**Hardware Requirements:** +- Minimum RAM: 1GB +- Minimum CPU cores: 2 +- Minimum storage: 5GB + +**Access:** +- Apache: http://localhost +- phpMyAdmin: http://localhost/phpmyadmin + +### 2. MEAN Stack + +MongoDB, Express.js, Angular, Node.js stack for modern web applications. + +```bash +cortex install --template mean --execute +``` + +**Packages:** +- Node.js 20.x +- MongoDB +- Angular CLI +- Express generator + +**Hardware Requirements:** +- Minimum RAM: 2GB +- Minimum CPU cores: 2 +- Minimum storage: 10GB + +### 3. MERN Stack + +MongoDB, Express.js, React, Node.js stack for full-stack JavaScript development. + +```bash +cortex install --template mern --execute +``` + +**Packages:** +- Node.js 20.x +- MongoDB +- React (via create-react-app) +- Express generator + +**Hardware Requirements:** +- Minimum RAM: 2GB +- Minimum CPU cores: 2 +- Minimum storage: 10GB + +### 4. ML/AI Stack + +Machine Learning and Artificial Intelligence development stack. + +```bash +cortex install --template ml-ai --execute +``` + +**Packages:** +- Python 3.x +- NumPy, Pandas, SciPy +- TensorFlow +- PyTorch +- Jupyter Notebook +- Scikit-learn +- Matplotlib, Seaborn + +**Hardware Requirements:** +- Minimum RAM: 4GB +- Minimum CPU cores: 4 +- Minimum storage: 20GB + +### 5. DevOps Stack + +Complete DevOps toolchain with containerization and infrastructure tools. + +```bash +cortex install --template devops --execute +``` + +**Packages:** +- Docker & Docker Compose +- Kubernetes (kubectl) +- Terraform +- Ansible +- Git +- Jenkins (optional) + +**Hardware Requirements:** +- Minimum RAM: 4GB +- Minimum CPU cores: 4 +- Minimum storage: 20GB + +## Template Format + +Templates are defined in YAML or JSON format. Here's the structure: + +### YAML Format + +```yaml +name: My Custom Stack +description: A custom development stack +version: 1.0.0 +author: Your Name + +packages: + - package1 + - package2 + - package3 + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y package1 package2 + description: Install packages + requires_root: true + rollback: apt remove -y package1 package2 + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + requires_gpu: false + requires_cuda: false + +post_install: + - echo "Stack installed successfully" + - echo "Access at: http://localhost" + +verification_commands: + - package1 --version + - systemctl is-active service1 + +metadata: + category: web-development + tags: + - web + - server +``` + +### JSON Format + +```json +{ + "name": "My Custom Stack", + "description": "A custom development stack", + "version": "1.0.0", + "author": "Your Name", + "packages": [ + "package1", + "package2" + ], + "steps": [ + { + "command": "apt update", + "description": "Update package lists", + "requires_root": true + } + ], + "hardware_requirements": { + "min_ram_mb": 2048, + "min_cores": 2, + "min_storage_mb": 10240 + }, + "post_install": [ + "echo 'Stack installed successfully'" + ], + "verification_commands": [ + "package1 --version" + ] +} +``` + +## Template Fields + +### Required Fields + +- **name**: Template name (string) +- **description**: Template description (string) +- **version**: Template version (string, e.g., "1.0.0") + +### Optional Fields + +- **author**: Template author (string) +- **packages**: List of package names (array of strings) +- **steps**: Installation steps (array of step objects) +- **hardware_requirements**: Hardware requirements (object) +- **post_install**: Post-installation commands (array of strings) +- **verification_commands**: Commands to verify installation (array of strings) +- **metadata**: Additional metadata (object) + +### Installation Steps + +Each step can have: +- **command**: Command to execute (required) +- **description**: Step description (required) +- **rollback**: Rollback command (optional) +- **verify**: Verification command (optional) +- **requires_root**: Whether root is required (boolean, default: true) + +### Hardware Requirements + +- **min_ram_mb**: Minimum RAM in megabytes (integer) +- **min_cores**: Minimum CPU cores (integer) +- **min_storage_mb**: Minimum storage in megabytes (integer) +- **requires_gpu**: Whether GPU is required (boolean) +- **gpu_vendor**: Required GPU vendor ("NVIDIA", "AMD", "Intel") +- **requires_cuda**: Whether CUDA is required (boolean) +- **min_cuda_version**: Minimum CUDA version (string, e.g., "11.0") + +## Creating Custom Templates + +### Interactive Creation + +```bash +cortex template create my-stack +``` + +This will prompt you for: +- Description +- Version +- Author (optional) +- Packages (one per line) +- Hardware requirements (optional) + +### Manual Creation + +1. Create a YAML or JSON file: + +```yaml +name: my-custom-stack +description: My custom development stack +version: 1.0.0 +packages: + - python3 + - nodejs + - docker +``` + +2. Import the template: + +```bash +cortex template import my-template.yaml +``` + +3. Use the template: + +```bash +cortex install --template my-custom-stack --execute +``` + +## Template Management + +### Listing Templates + +```bash +cortex template list +``` + +Output: +``` +šŸ“‹ Available Templates: +================================================================================ +Name Version Type Description +================================================================================ +devops 1.0.0 built-in Complete DevOps toolchain... +lamp 1.0.0 built-in Linux, Apache, MySQL, PHP... +mean 1.0.0 built-in MongoDB, Express.js, Angular... +mern 1.0.0 built-in MongoDB, Express.js, React... +ml-ai 1.0.0 built-in Machine Learning and AI... + +Total: 5 templates +``` + +### Exporting Templates + +```bash +# Export to YAML (default) +cortex template export lamp my-lamp-template.yaml + +# Export to JSON +cortex template export lamp my-lamp-template.json --format json +``` + +### Importing Templates + +```bash +# Import with original name +cortex template import my-template.yaml + +# Import with custom name +cortex template import my-template.yaml --name my-custom-name +``` + +## Hardware Compatibility + +Templates can specify hardware requirements. Cortex will check compatibility before installation: + +```bash +$ cortex install --template ml-ai --execute + +šŸ“‹ ML/AI Stack Template: + Machine Learning and Artificial Intelligence development stack + + Packages: + - python3 + - python3-pip + ... + +āš ļø Hardware Compatibility Warnings: + - Insufficient RAM: 2048MB available, 4096MB required + +āš ļø Hardware requirements not met. Continue anyway? (y/N): +``` + +## Template Validation + +Templates are automatically validated before installation. Validation checks: + +- Required fields are present +- At least packages or steps are defined +- Step commands and descriptions are provided +- Hardware requirements are valid (non-negative values) +- CUDA requirements are consistent with GPU requirements + +## Example Templates + +### Python Data Science Stack + +```yaml +name: Python Data Science +description: Python with data science libraries +version: 1.0.0 +packages: + - python3 + - python3-pip + - python3-venv +steps: + - command: pip3 install numpy pandas scipy matplotlib jupyter scikit-learn + description: Install data science libraries + requires_root: false +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 +``` + +### Docker Development Stack + +```yaml +name: Docker Development +description: Docker with development tools +version: 1.0.0 +packages: + - docker.io + - docker-compose + - git +steps: + - command: systemctl start docker + description: Start Docker service + requires_root: true + rollback: systemctl stop docker + - command: systemctl enable docker + description: Enable Docker on boot + requires_root: true +verification_commands: + - docker --version + - docker ps +``` + +### Full-Stack Web Development + +```yaml +name: Full-Stack Web +description: Complete web development environment +version: 1.0.0 +packages: + - nodejs + - npm + - python3 + - postgresql + - redis-server + - nginx +steps: + - command: npm install -g yarn typescript + description: Install global Node.js tools + requires_root: false + - command: systemctl start postgresql + description: Start PostgreSQL + requires_root: true + - command: systemctl start redis + description: Start Redis + requires_root: true +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 +post_install: + - echo "Web development stack ready!" + - echo "PostgreSQL: localhost:5432" + - echo "Redis: localhost:6379" +``` + +## Best Practices + +1. **Always test templates in dry-run mode first:** + ```bash + cortex install --template my-template --dry-run + ``` + +2. **Specify hardware requirements** to help users understand system needs + +3. **Include verification commands** to ensure installation succeeded + +4. **Add rollback commands** for critical steps to enable safe rollback + +5. **Use descriptive step descriptions** for better user experience + +6. **Version your templates** to track changes + +7. **Document post-installation steps** in post_install commands + +## Troubleshooting + +### Template Not Found + +If a template is not found, check: +- Template name is correct (use `cortex template list` to verify) +- Template file exists in `~/.cortex/templates/` or built-in templates directory +- File extension is `.yaml`, `.yml`, or `.json` + +### Validation Errors + +If template validation fails: +- Check all required fields are present +- Ensure at least packages or steps are defined +- Verify hardware requirements are non-negative +- Check step commands and descriptions are provided + +### Hardware Compatibility Warnings + +If hardware compatibility warnings appear: +- Review the warnings carefully +- Consider if the installation will work with your hardware +- Some templates may work with less hardware but with reduced performance +- You can proceed anyway if you understand the risks + +## Template Sharing + +Templates can be shared by: +1. Exporting to a file +2. Sharing the file via version control, email, or file sharing +3. Importing on another system + +Example workflow: +```bash +# On source system +cortex template export my-stack my-stack.yaml + +# Share my-stack.yaml + +# On target system +cortex template import my-stack.yaml +cortex install --template my-stack --execute +``` + +## Advanced Usage + +### Using Steps Instead of Packages + +For more control, use explicit installation steps: + +```yaml +steps: + - command: apt update + description: Update package lists + - command: curl -fsSL https://get.docker.com | sh + description: Install Docker + rollback: apt remove -y docker docker-engine + - command: systemctl start docker + description: Start Docker service + verify: systemctl is-active docker +``` + +### Conditional Installation + +While templates don't support conditional logic directly, you can use shell commands: + +```yaml +steps: + - command: | + if [ ! -f /usr/bin/docker ]; then + apt install -y docker.io + fi + description: Install Docker if not present +``` + +### Post-Installation Configuration + +Use post_install commands for configuration: + +```yaml +post_install: + - echo "Configuring service..." + - systemctl enable myservice + - echo "Service configured. Access at http://localhost:8080" +``` + +## See Also + +- [User Guide](../User-Guide.md) - General Cortex usage +- [Developer Guide](../Developer-Guide.md) - Contributing to Cortex +- [Getting Started](../Getting-Started.md) - Quick start guide + diff --git a/src/requirements.txt b/src/requirements.txt index 65c3c15..4e2d620 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -4,6 +4,7 @@ # Core Dependencies rich>=13.0.0 # Beautiful terminal progress bars and formatting plyer>=2.0.0 # Desktop notifications (optional but recommended) +pyyaml>=6.0.0 # YAML parsing for template system # Testing Dependencies (dev) pytest>=7.0.0 diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 0000000..553449a --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +""" +Unit tests for Cortex Linux Template System +""" + +import unittest +import tempfile +import shutil +import os +import json +import yaml +from pathlib import Path + +import sys +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.templates import ( + Template, + TemplateManager, + TemplateValidator, + TemplateFormat, + HardwareRequirements, + InstallationStep +) + + +class TestTemplate(unittest.TestCase): + """Test Template dataclass.""" + + def test_template_creation(self): + """Test creating a template.""" + template = Template( + name="test-template", + description="Test template", + version="1.0.0", + author="Test Author", + packages=["package1", "package2"] + ) + + self.assertEqual(template.name, "test-template") + self.assertEqual(template.description, "Test template") + self.assertEqual(template.version, "1.0.0") + self.assertEqual(template.author, "Test Author") + self.assertEqual(len(template.packages), 2) + + def test_template_to_dict(self): + """Test converting template to dictionary.""" + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1", "pkg2"] + ) + + data = template.to_dict() + + self.assertEqual(data["name"], "test") + self.assertEqual(data["description"], "Test") + self.assertEqual(data["version"], "1.0.0") + self.assertEqual(data["packages"], ["pkg1", "pkg2"]) + + def test_template_from_dict(self): + """Test creating template from dictionary.""" + data = { + "name": "test", + "description": "Test description", + "version": "1.0.0", + "packages": ["pkg1", "pkg2"], + "steps": [ + { + "command": "apt install pkg1", + "description": "Install package 1", + "requires_root": True + } + ] + } + + template = Template.from_dict(data) + + self.assertEqual(template.name, "test") + self.assertEqual(template.description, "Test description") + self.assertEqual(len(template.packages), 2) + self.assertEqual(len(template.steps), 1) + self.assertEqual(template.steps[0].command, "apt install pkg1") + + def test_template_with_hardware_requirements(self): + """Test template with hardware requirements.""" + hw_req = HardwareRequirements( + min_ram_mb=4096, + min_cores=4, + requires_gpu=True, + gpu_vendor="NVIDIA" + ) + + template = Template( + name="gpu-template", + description="GPU template", + version="1.0.0", + hardware_requirements=hw_req + ) + + self.assertIsNotNone(template.hardware_requirements) + self.assertEqual(template.hardware_requirements.min_ram_mb, 4096) + self.assertEqual(template.hardware_requirements.requires_gpu, True) + + +class TestTemplateValidator(unittest.TestCase): + """Test TemplateValidator.""" + + def test_validate_valid_template(self): + """Test validating a valid template.""" + template = Template( + name="valid-template", + description="Valid template", + version="1.0.0", + packages=["pkg1"] + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertTrue(is_valid) + self.assertEqual(len(errors), 0) + + def test_validate_missing_name(self): + """Test validating template with missing name.""" + template = Template( + name="", + description="Test", + version="1.0.0" + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("name is required", errors[0]) + + def test_validate_missing_packages_and_steps(self): + """Test validating template with no packages or steps.""" + template = Template( + name="empty-template", + description="Empty template", + version="1.0.0" + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("packages or steps", errors[0]) + + def test_validate_invalid_hardware_requirements(self): + """Test validating template with invalid hardware requirements.""" + hw_req = HardwareRequirements(min_ram_mb=-1) + template = Template( + name="invalid-hw", + description="Invalid hardware", + version="1.0.0", + packages=["pkg1"], + hardware_requirements=hw_req + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("min_ram_mb", errors[0]) + + +class TestTemplateManager(unittest.TestCase): + """Test TemplateManager.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.template_dir = Path(self.temp_dir) / "templates" + self.template_dir.mkdir() + + # Create a test template + self.test_template_data = { + "name": "test-template", + "description": "Test template", + "version": "1.0.0", + "packages": ["package1", "package2"], + "steps": [ + { + "command": "apt update", + "description": "Update packages", + "requires_root": True + } + ] + } + + # Write test template to file + template_file = self.template_dir / "test-template.yaml" + with open(template_file, 'w') as f: + yaml.dump(self.test_template_data, f) + + def tearDown(self): + """Clean up test fixtures.""" + shutil.rmtree(self.temp_dir) + + def test_load_template(self): + """Test loading a template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + template = manager.load_template("test-template") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "test-template") + self.assertEqual(len(template.packages), 2) + + def test_load_nonexistent_template(self): + """Test loading a non-existent template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + template = manager.load_template("nonexistent") + + self.assertIsNone(template) + + def test_save_template(self): + """Test saving a template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="new-template", + description="New template", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "new-template") + + self.assertTrue(template_path.exists()) + + # Verify it can be loaded + loaded = manager.load_template("new-template") + self.assertIsNotNone(loaded) + self.assertEqual(loaded.name, "new-template") + + def test_list_templates(self): + """Test listing templates.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + templates = manager.list_templates() + + self.assertIn("test-template", templates) + self.assertEqual(templates["test-template"]["name"], "test-template") + + def test_generate_commands_from_packages(self): + """Test generating commands from packages.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["package1", "package2"] + ) + + commands = manager.generate_commands(template) + + self.assertGreater(len(commands), 0) + self.assertTrue(any("package1" in cmd or "package2" in cmd for cmd in commands)) + + def test_generate_commands_from_steps(self): + """Test generating commands from steps.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="test", + description="Test", + version="1.0.0", + steps=[ + InstallationStep( + command="apt install pkg1", + description="Install pkg1" + ) + ] + ) + + commands = manager.generate_commands(template) + + self.assertEqual(len(commands), 1) + self.assertEqual(commands[0], "apt install pkg1") + + def test_import_template(self): + """Test importing a template from file.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + # Create a temporary template file + temp_file = Path(self.temp_dir) / "import-template.yaml" + with open(temp_file, 'w') as f: + yaml.dump(self.test_template_data, f) + + template = manager.import_template(str(temp_file)) + + self.assertIsNotNone(template) + self.assertEqual(template.name, "test-template") + + def test_export_template(self): + """Test exporting a template to file.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + export_path = Path(self.temp_dir) / "exported-template.yaml" + manager.export_template("test-template", str(export_path)) + + self.assertTrue(export_path.exists()) + + # Verify content + with open(export_path, 'r') as f: + data = yaml.safe_load(f) + self.assertEqual(data["name"], "test-template") + + +class TestHardwareCompatibility(unittest.TestCase): + """Test hardware compatibility checking.""" + + def setUp(self): + """Set up test fixtures.""" + self.manager = TemplateManager() + + def test_check_hardware_compatibility_no_requirements(self): + """Test checking compatibility with no requirements.""" + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1"] + ) + + is_compatible, warnings = self.manager.check_hardware_compatibility(template) + + self.assertTrue(is_compatible) + self.assertEqual(len(warnings), 0) + + def test_check_hardware_compatibility_with_requirements(self): + """Test checking compatibility with requirements.""" + hw_req = HardwareRequirements( + min_ram_mb=1024, + min_cores=2 + ) + + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1"], + hardware_requirements=hw_req + ) + + is_compatible, warnings = self.manager.check_hardware_compatibility(template) + + # Result depends on actual hardware, but should not crash + self.assertIsInstance(is_compatible, bool) + self.assertIsInstance(warnings, list) + + +class TestTemplateFormat(unittest.TestCase): + """Test template format handling.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.template_dir = Path(self.temp_dir) / "templates" + self.template_dir.mkdir() + + def tearDown(self): + """Clean up test fixtures.""" + shutil.rmtree(self.temp_dir) + + def test_save_yaml_format(self): + """Test saving template in YAML format.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="yaml-test", + description="YAML test", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "yaml-test", TemplateFormat.YAML) + + self.assertTrue(template_path.exists()) + self.assertEqual(template_path.suffix, ".yaml") + + # Verify it's valid YAML + with open(template_path, 'r') as f: + data = yaml.safe_load(f) + self.assertEqual(data["name"], "yaml-test") + + def test_save_json_format(self): + """Test saving template in JSON format.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="json-test", + description="JSON test", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "json-test", TemplateFormat.JSON) + + self.assertTrue(template_path.exists()) + self.assertEqual(template_path.suffix, ".json") + + # Verify it's valid JSON + with open(template_path, 'r') as f: + data = json.load(f) + self.assertEqual(data["name"], "json-test") + + def test_load_yaml_template(self): + """Test loading YAML template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template_data = { + "name": "yaml-load", + "description": "YAML load test", + "version": "1.0.0", + "packages": ["pkg1"] + } + + template_file = self.template_dir / "yaml-load.yaml" + with open(template_file, 'w') as f: + yaml.dump(template_data, f) + + template = manager.load_template("yaml-load") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "yaml-load") + + def test_load_json_template(self): + """Test loading JSON template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template_data = { + "name": "json-load", + "description": "JSON load test", + "version": "1.0.0", + "packages": ["pkg1"] + } + + template_file = self.template_dir / "json-load.json" + with open(template_file, 'w') as f: + json.dump(template_data, f) + + template = manager.load_template("json-load") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "json-load") + + +if __name__ == '__main__': + unittest.main() + From 5f2dff371b53dbc81dede7c0fd6c7bf89b5dc27a Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:12:39 +0500 Subject: [PATCH 2/5] fix: Address security and installation issues in template system --- cortex/cli.py | 14 ++++---------- cortex/templates.py | 30 +++++++++++++++++++----------- cortex/templates/lamp.yaml | 15 ++++++++++++--- cortex/templates/mean.yaml | 31 ++++++++++++++++++++----------- cortex/templates/mern.yaml | 31 ++++++++++++++++++++----------- cortex/templates/ml-ai.yaml | 6 +++--- 6 files changed, 78 insertions(+), 49 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index d9fe5bd..454b4e5 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -298,7 +298,7 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo # Check hardware compatibility is_compatible, warnings = template_manager.check_hardware_compatibility(template) if warnings: - print(f"\n[WARNING] Hardware Compatibility Warnings:") + print("\n[WARNING] Hardware Compatibility Warnings:") for warning in warnings: print(f" - {warning}") if not is_compatible and not dry_run: @@ -391,22 +391,16 @@ def progress_callback(current, total, step): status = "[OK]" if passed else "[FAIL]" print(f" {status} {cmd}") - # Run post-install commands + # Run post-install commands once if template.post_install: self._print_status("[*]", "Running post-installation steps...") + print("\n[*] Post-installation information:") for cmd in template.post_install: subprocess.run(cmd, shell=True) self._print_success(f"{template.name} stack ready!") print(f"\nCompleted in {result.total_duration:.2f} seconds") - # Display post-install info - if template.post_install: - print("\n[*] Post-installation information:") - for cmd in template.post_install: - if cmd.startswith("echo"): - subprocess.run(cmd, shell=True) - # Record successful installation if install_id: history.update_installation(install_id, InstallationStatus.SUCCESS) @@ -445,7 +439,7 @@ def progress_callback(current, total, step): history.update_installation(install_id, InstallationStatus.FAILED, str(e)) self._print_error(str(e)) return 1 - except Exception as e: + except (RuntimeError, OSError, subprocess.SubprocessError) as e: if install_id: history.update_installation(install_id, InstallationStatus.FAILED, str(e)) self._print_error(f"Unexpected error: {str(e)}") diff --git a/cortex/templates.py b/cortex/templates.py index f4dd7f6..3ceab92 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -296,23 +296,31 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: """List all available templates.""" templates = {} - # List built-in templates + # List built-in templates (load directly from file to avoid user overrides) if self.templates_dir.exists(): for ext in [".yaml", ".yml", ".json"]: for template_file in self.templates_dir.glob(f"*{ext}"): name = template_file.stem + if name in templates: + # Skip duplicate names across extensions + continue try: - template = self.load_template(name) - if template: - templates[name] = { - "name": template.name, - "description": template.description, - "version": template.version, - "author": template.author, - "type": "built-in", - "path": str(template_file) - } + with open(template_file, 'r', encoding='utf-8') as f: + if template_file.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + template = Template.from_dict(data) + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "built-in", + "path": str(template_file) + } except Exception: + # Ignore malformed built-ins but continue listing others pass # List user templates diff --git a/cortex/templates/lamp.yaml b/cortex/templates/lamp.yaml index 5421437..3e011e5 100644 --- a/cortex/templates/lamp.yaml +++ b/cortex/templates/lamp.yaml @@ -18,11 +18,17 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php + - command: echo "mysql-server mysql-server/root_password password temp_password" | debconf-set-selections && echo "mysql-server mysql-server/root_password_again password temp_password" | debconf-set-selections + description: Pre-configure MySQL root password + requires_root: true + - command: DEBIAN_FRONTEND=noninteractive apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php description: Install LAMP stack packages requires_root: true - - command: apt install -y phpmyadmin - description: Install phpMyAdmin + - command: echo "phpmyadmin phpmyadmin/dbconfig-install boolean true" | debconf-set-selections && echo "phpmyadmin phpmyadmin/reconfigure-webserver multiselect apache2" | debconf-set-selections && DEBIAN_FRONTEND=noninteractive apt install -y phpmyadmin + description: Install and configure phpMyAdmin for Apache + requires_root: true + - command: ln -sf /usr/share/phpmyadmin /var/www/html/phpmyadmin + description: Create phpMyAdmin symlink requires_root: true - command: systemctl start apache2 description: Start Apache web server @@ -48,8 +54,11 @@ hardware_requirements: post_install: - echo "LAMP stack installed successfully" + - echo "SECURITY: Run 'mysql_secure_installation' to secure MySQL" + - echo "SECURITY: Configure firewall rules for production use" - echo "Apache: http://localhost" - echo "phpMyAdmin: http://localhost/phpmyadmin" + - echo "SECURITY: Change default MySQL passwords before production use" verification_commands: - apache2 -v diff --git a/cortex/templates/mean.yaml b/cortex/templates/mean.yaml index 21bb9c6..0778e6f 100644 --- a/cortex/templates/mean.yaml +++ b/cortex/templates/mean.yaml @@ -13,26 +13,35 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - description: Add Node.js repository + - command: curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm /tmp/nodesource_setup.sh + description: Download and add Node.js repository requires_root: true - command: apt install -y nodejs description: Install Node.js requires_root: true - command: npm install -g @angular/cli express-generator description: Install Angular CLI and Express generator globally - requires_root: false - - command: apt install -y mongodb - description: Install MongoDB requires_root: true - - command: systemctl start mongodb + - command: curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + description: Import MongoDB GPG key + requires_root: true + - command: echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + description: Add MongoDB official apt repository + requires_root: true + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y mongodb-org + description: Install MongoDB from official repository + requires_root: true + - command: systemctl start mongod description: Start MongoDB service requires_root: true - rollback: systemctl stop mongodb - - command: systemctl enable mongodb + rollback: systemctl stop mongod + - command: systemctl enable mongod description: Enable MongoDB on boot requires_root: true - rollback: systemctl disable mongodb + rollback: systemctl disable mongod hardware_requirements: min_ram_mb: 2048 @@ -49,8 +58,8 @@ post_install: verification_commands: - node --version - npm --version - - mongod --version - - systemctl is-active mongodb + - mongosh --version || mongo --version + - systemctl is-active mongod - ng version metadata: diff --git a/cortex/templates/mern.yaml b/cortex/templates/mern.yaml index 953606a..59d84e8 100644 --- a/cortex/templates/mern.yaml +++ b/cortex/templates/mern.yaml @@ -13,26 +13,35 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - description: Add Node.js repository + - command: curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm /tmp/nodesource_setup.sh + description: Download and add Node.js repository requires_root: true - command: apt install -y nodejs description: Install Node.js requires_root: true - command: npm install -g create-react-app express-generator description: Install React and Express generators globally - requires_root: false - - command: apt install -y mongodb - description: Install MongoDB requires_root: true - - command: systemctl start mongodb + - command: curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + description: Import MongoDB GPG key + requires_root: true + - command: echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + description: Add MongoDB official apt repository + requires_root: true + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y mongodb-org + description: Install MongoDB from official repository + requires_root: true + - command: systemctl start mongod description: Start MongoDB service requires_root: true - rollback: systemctl stop mongodb - - command: systemctl enable mongodb + rollback: systemctl stop mongod + - command: systemctl enable mongod description: Enable MongoDB on boot requires_root: true - rollback: systemctl disable mongodb + rollback: systemctl disable mongod hardware_requirements: min_ram_mb: 2048 @@ -49,8 +58,8 @@ post_install: verification_commands: - node --version - npm --version - - mongod --version - - systemctl is-active mongodb + - mongosh --version || mongo --version + - systemctl is-active mongod - npx create-react-app --version metadata: diff --git a/cortex/templates/ml-ai.yaml b/cortex/templates/ml-ai.yaml index 5880961..681385a 100644 --- a/cortex/templates/ml-ai.yaml +++ b/cortex/templates/ml-ai.yaml @@ -1,5 +1,5 @@ name: ML/AI Stack -description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter +description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter. GPU recommended for deep learning workloads. version: 1.0.0 author: Cortex Linux packages: @@ -33,9 +33,9 @@ steps: requires_root: false hardware_requirements: - min_ram_mb: 4096 + min_ram_mb: 8192 min_cores: 4 - min_storage_mb: 20480 + min_storage_mb: 30720 requires_gpu: false requires_cuda: false From ea9c9fa7a12b695c7295839908514b13966a58f5 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:31:47 +0500 Subject: [PATCH 3/5] fix: Security and installation fixes for template system --- cortex/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex/cli.py b/cortex/cli.py index 454b4e5..be88148 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -305,10 +305,12 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo try: response = input("\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): ") if response.lower() != 'y': + print("\n[INFO] Installation aborted by user") return 1 except (EOFError, KeyboardInterrupt): # Non-interactive environment or user cancelled - print("\n[INFO] Skipping hardware check prompt (non-interactive mode)") + print("\n[ERROR] Aborting install: cannot prompt for hardware confirmation in non-interactive mode") + print(" Use --dry-run to preview commands, or ensure hardware requirements are met") return 1 # Generate commands From bfaa85b372ba200097e7c18dcec34be0646e2e4b Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:51:16 +0500 Subject: [PATCH 4/5] feat: Add installation templates system for common development stacks --- cortex/cli.py | 27 +++++++++++--------- cortex/templates.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index be88148..97613c3 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -511,11 +511,15 @@ def template_create(self, name: str, interactive: bool = True): min_storage = input(" Minimum storage (MB, optional): ").strip() if min_ram or min_cores or min_storage: - hw_req = HardwareRequirements( - min_ram_mb=int(min_ram) if min_ram else None, - min_cores=int(min_cores) if min_cores else None, - min_storage_mb=int(min_storage) if min_storage else None - ) + try: + hw_req = HardwareRequirements( + min_ram_mb=int(min_ram) if min_ram else None, + min_cores=int(min_cores) if min_cores else None, + min_storage_mb=int(min_storage) if min_storage else None + ) + except ValueError: + self._print_error("Hardware requirements must be numeric values") + return 1 template.hardware_requirements = hw_req # Save template @@ -595,8 +599,9 @@ def main(): # Install command install_parser = subparsers.add_parser('install', help='Install software using natural language or template') - install_parser.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') - install_parser.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') + install_group = install_parser.add_mutually_exclusive_group(required=True) + install_group.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') + install_group.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') install_parser.add_argument('--execute', action='store_true', help='Execute the generated commands') install_parser.add_argument('--dry-run', action='store_true', help='Show commands without executing') @@ -617,7 +622,7 @@ def main(): template_subparsers = template_parser.add_subparsers(dest='template_action', help='Template actions') # Template list - template_list_parser = template_subparsers.add_parser('list', help='List all available templates') + template_subparsers.add_parser('list', help='List all available templates') # Template create template_create_parser = template_subparsers.add_parser('create', help='Create a new template') @@ -646,11 +651,9 @@ def main(): if args.command == 'install': if args.template: return cli.install("", execute=args.execute, dry_run=args.dry_run, template=args.template) - elif args.software: - return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) else: - install_parser.print_help() - return 1 + # software is guaranteed to be set due to mutually_exclusive_group(required=True) + return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) elif args.command == 'history': return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) elif args.command == 'rollback': diff --git a/cortex/templates.py b/cortex/templates.py index 3ceab92..1943fa2 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -154,6 +154,63 @@ class TemplateValidator: REQUIRED_FIELDS = ["name", "description", "version"] REQUIRED_STEP_FIELDS = ["command", "description"] + # Allowed post_install commands (whitelist for security) + ALLOWED_POST_INSTALL_COMMANDS = { + 'echo', # Safe echo commands + } + + # Dangerous shell metacharacters that should be rejected + DANGEROUS_SHELL_CHARS = [';', '|', '&', '>', '<', '`', '\\'] + + @staticmethod + def _validate_post_install_commands(post_install: List[str]) -> List[str]: + """ + Validate post_install commands for security. + + Returns: + List of validation errors + """ + errors = [] + + for i, cmd in enumerate(post_install): + if not cmd or not cmd.strip(): + continue + + cmd_stripped = cmd.strip() + + # Check for dangerous shell metacharacters + for char in TemplateValidator.DANGEROUS_SHELL_CHARS: + if char in cmd_stripped: + errors.append( + f"post_install[{i}]: Contains dangerous shell character '{char}'. " + "Only safe commands like 'echo' are allowed." + ) + break + + # Check for command substitution patterns + if '$(' in cmd_stripped or '`' in cmd_stripped: + # Allow $(...) in echo commands for version checks (built-in templates use this) + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Command substitution only allowed in 'echo' commands" + ) + + # Check for wildcards/globs + if '*' in cmd_stripped or '?' in cmd_stripped: + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Wildcards only allowed in 'echo' commands" + ) + + # Whitelist check - only allow echo commands + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Only 'echo' commands are allowed in post_install. " + f"Found: {cmd_stripped[:50]}" + ) + + return errors + @staticmethod def validate(template: Template) -> Tuple[bool, List[str]]: """ @@ -195,6 +252,11 @@ def validate(template: Template) -> Tuple[bool, List[str]]: if hw.requires_cuda and not hw.requires_gpu: errors.append("requires_cuda is true but requires_gpu is false") + # Validate post_install commands for security + if template.post_install: + post_install_errors = TemplateValidator._validate_post_install_commands(template.post_install) + errors.extend(post_install_errors) + return len(errors) == 0, errors From 0698ae0d628c610f7794fdbbf8d6d25585cce19b Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 29 Nov 2025 00:17:28 +0500 Subject: [PATCH 5/5] fix unit tests --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 25a4cd2..f2ad73b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,8 @@ anthropic>=0.18.0 openai>=1.0.0 +# YAML parsing for template system +PyYAML>=6.0 + # Type hints for older Python versions typing-extensions>=4.0.0