Files
troubleshoot/pkg/analyze/ollama_helper.go
Benjamin Yang 21dc4e9b09 Fix ollama windows installer (#1894)
* Fix Windows filename issue in scheduled support bundles

* Fix: Close temp file before executing Ollama installer on Windows

Windows requires files to be closed before they can be executed. This fix
ensures the temporary installer file is properly closed before attempting
to run it, preventing file access errors on Windows systems.
2025-10-14 10:51:52 -05:00

423 lines
11 KiB
Go

package analyzer
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/pkg/errors"
"k8s.io/klog/v2"
)
// OllamaHelper provides utilities for downloading and managing Ollama
type OllamaHelper struct {
downloadURL string
installPath string
checkInterval time.Duration
}
// NewOllamaHelper creates a new Ollama helper with platform-specific defaults
func NewOllamaHelper() *OllamaHelper {
return &OllamaHelper{
downloadURL: getOllamaDownloadURL(),
installPath: getOllamaInstallPath(),
checkInterval: 30 * time.Second,
}
}
// IsInstalled checks if Ollama is already installed and available
func (h *OllamaHelper) IsInstalled() bool {
_, err := exec.LookPath("ollama")
return err == nil
}
// IsRunning checks if Ollama service is currently running
func (h *OllamaHelper) IsRunning() bool {
cmd := exec.Command("ollama", "ps")
err := cmd.Run()
return err == nil
}
// GetInstallInstructions returns platform-specific installation instructions
func (h *OllamaHelper) GetInstallInstructions() string {
instructions := `
To use Ollama for advanced AI-powered analysis, you need to install Ollama:
🔧 Installation Options:
1. **Automatic Download** (recommended):
Run: troubleshoot analyze --setup-ollama
2. **Manual Installation**:
`
switch runtime.GOOS {
case "darwin":
instructions += ` • Visit: https://ollama.ai/download
• Download and install Ollama for macOS
• Or use Homebrew: brew install ollama`
case "linux":
instructions += ` • Run: curl -fsSL https://ollama.ai/install.sh | sh
• Or download from: https://ollama.ai/download`
case "windows":
instructions += ` • Visit: https://ollama.ai/download
• Download and install Ollama for Windows`
default:
instructions += ` • Visit: https://ollama.ai/download
• Download the appropriate version for your platform`
}
instructions += `
3. **Docker** (alternative):
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
📋 After installation:
1. Start Ollama: ollama serve
2. Pull a model: ollama pull llama2:7b
3. Run analysis: troubleshoot analyze --enable-ollama bundle.tar.gz
🔍 Verify installation: ollama --version
`
return instructions
}
// GetSetupCommand returns the command to start Ollama service
func (h *OllamaHelper) GetSetupCommand() string {
return `# Start Ollama service in background
ollama serve &
# Pull recommended model for troubleshooting
ollama pull llama2:7b
# Verify it's working
ollama ps`
}
// DownloadAndInstall automatically downloads and installs Ollama
func (h *OllamaHelper) DownloadAndInstall() error {
if h.IsInstalled() {
return errors.New("Ollama is already installed")
}
klog.Info("Downloading Ollama...")
switch runtime.GOOS {
case "darwin":
// For macOS, try Homebrew first, fall back to direct download
return h.installMacOS()
case "linux":
// For Linux, use the official install script
klog.Info("Running official Ollama install script...")
cmd := exec.Command("sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "installation script failed")
}
case "windows":
// For Windows, download and run the installer
return h.downloadAndInstallWindows()
default:
return errors.Errorf("unsupported platform: %s", runtime.GOOS)
}
klog.Info("Ollama installed successfully!")
return nil
}
// downloadAndInstallWindows handles Windows-specific installation
func (h *OllamaHelper) downloadAndInstallWindows() error {
// Create temporary file
tmpFile, err := os.CreateTemp("", "ollama-installer-*.exe")
if err != nil {
return errors.Wrap(err, "failed to create temporary file")
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close() // Ensures file is closed in error paths
// Download installer
resp, err := http.Get(h.downloadURL)
if err != nil {
return errors.Wrap(err, "failed to download Ollama installer")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.Errorf("download failed with status %d", resp.StatusCode)
}
// Write to temporary file
_, err = io.Copy(tmpFile, resp.Body)
if err != nil {
return errors.Wrap(err, "failed to write installer")
}
// Close the file before executing it (required on Windows)
// Note: This will be called twice (here and via defer), but that's safe
// The defer ensures cleanup on error paths, this ensures closure before execution
if err := tmpFile.Close(); err != nil {
return errors.Wrap(err, "failed to close installer file")
}
// Run installer
klog.Info("Running Ollama installer...")
cmd := exec.Command(tmpFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "installation failed")
}
return nil
}
// installMacOS handles macOS-specific installation using Homebrew
func (h *OllamaHelper) installMacOS() error {
// Check if Homebrew is available
if _, err := exec.LookPath("brew"); err != nil {
return errors.New("Homebrew is required for automatic installation on macOS. Please install Homebrew first or install Ollama manually from https://ollama.com/download")
}
klog.Info("Installing Ollama via Homebrew...")
cmd := exec.Command("brew", "install", "ollama")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "Homebrew installation failed")
}
return nil
}
// StartService starts the Ollama service
func (h *OllamaHelper) StartService() error {
if !h.IsInstalled() {
return errors.New("Ollama is not installed")
}
if h.IsRunning() {
klog.Info("Ollama service is already running")
return nil
}
klog.Info("Starting Ollama service...")
// Start ollama serve in background
cmd := exec.Command("ollama", "serve")
// Start in background
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to start Ollama service")
}
// Wait a moment for service to start
time.Sleep(3 * time.Second)
// Verify it's running
if !h.IsRunning() {
return errors.New("Ollama service failed to start properly")
}
klog.Info("Ollama service started successfully!")
return nil
}
// PullModel downloads a specific model for use with Ollama
func (h *OllamaHelper) PullModel(model string) error {
if !h.IsRunning() {
return errors.New("Ollama service is not running. Start it with: ollama serve")
}
klog.Infof("Pulling model: %s (this may take several minutes)...", model)
cmd := exec.Command("ollama", "pull", model)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "failed to pull model %s", model)
}
klog.Infof("Model %s pulled successfully!", model)
return nil
}
// ListAvailableModels returns a list of recommended models for troubleshooting
func (h *OllamaHelper) ListAvailableModels() []ModelInfo {
return []ModelInfo{
{
Name: "llama2:7b",
Size: "3.8GB",
Description: "General purpose model, good balance of performance and resource usage",
Recommended: true,
},
{
Name: "llama2:13b",
Size: "7.3GB",
Description: "Better analysis quality but requires more memory",
Recommended: false,
},
{
Name: "codellama:7b",
Size: "3.8GB",
Description: "Specialized for code analysis and technical content",
Recommended: true,
},
{
Name: "codellama:13b",
Size: "7.3GB",
Description: "Advanced code analysis, higher quality but resource intensive",
Recommended: false,
},
{
Name: "mistral:7b",
Size: "4.1GB",
Description: "Fast and efficient model for quick analysis",
Recommended: false,
},
}
}
// ModelInfo contains information about available models
type ModelInfo struct {
Name string
Size string
Description string
Recommended bool
}
// PrintModelRecommendations prints user-friendly model selection guide
func (h *OllamaHelper) PrintModelRecommendations() {
fmt.Println("\n📚 Recommended Models for Troubleshooting:")
fmt.Println("=" + strings.Repeat("=", 50))
for _, model := range h.ListAvailableModels() {
status := " "
if model.Recommended {
status = "⭐"
}
fmt.Printf("%s %s (%s)\n", status, model.Name, model.Size)
fmt.Printf(" %s\n", model.Description)
if model.Recommended {
fmt.Printf(" 💡 Pull with: ollama pull %s\n", model.Name)
}
fmt.Println()
}
fmt.Println("💡 For beginners: Start with 'llama2:7b' or 'codellama:7b'")
fmt.Println("🔧 For advanced users: Try 'llama2:13b' if you have enough RAM")
}
// OllamaHealthStatus represents the current state of Ollama
type OllamaHealthStatus struct {
Installed bool
Running bool
Models []string
Endpoint string
}
// GetHealthStatus returns the current status of Ollama installation and service
func (h *OllamaHelper) GetHealthStatus() OllamaHealthStatus {
status := OllamaHealthStatus{
Installed: h.IsInstalled(),
Running: false,
Models: []string{},
Endpoint: "http://localhost:11434",
}
if status.Installed {
status.Running = h.IsRunning()
if status.Running {
// Get list of installed models
cmd := exec.Command("ollama", "list")
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, ":") && !strings.Contains(line, "NAME") {
parts := strings.Fields(line)
if len(parts) > 0 {
status.Models = append(status.Models, parts[0])
}
}
}
}
}
}
return status
}
// String returns a human-readable status summary
func (hs OllamaHealthStatus) String() string {
var status strings.Builder
status.WriteString("🔍 Ollama Status:\n")
if hs.Installed {
status.WriteString("✅ Installed\n")
if hs.Running {
status.WriteString("✅ Service Running\n")
status.WriteString(fmt.Sprintf("🌐 Endpoint: %s\n", hs.Endpoint))
if len(hs.Models) > 0 {
status.WriteString(fmt.Sprintf("📚 Models Available: %s\n", strings.Join(hs.Models, ", ")))
} else {
status.WriteString("⚠️ No models installed. Run: ollama pull llama2:7b\n")
}
} else {
status.WriteString("⚠️ Service Not Running. Start with: ollama serve\n")
}
} else {
status.WriteString("❌ Not Installed\n")
status.WriteString("💡 Install with: troubleshoot analyze --setup-ollama\n")
}
return status.String()
}
// getOllamaDownloadURL returns the platform-specific download URL
func getOllamaDownloadURL() string {
switch runtime.GOOS {
case "darwin", "linux":
// Use the official install script for both macOS and Linux
return "https://ollama.com/install.sh"
case "windows":
return "https://ollama.com/download/OllamaSetup.exe"
default:
return "https://ollama.com/install.sh"
}
}
// getOllamaInstallPath returns the platform-specific install path
func getOllamaInstallPath() string {
switch runtime.GOOS {
case "darwin":
return "/usr/local/bin/ollama"
case "linux":
return "/usr/local/bin/ollama"
case "windows":
return "C:\\Program Files\\Ollama\\ollama.exe"
default:
return "/usr/local/bin/ollama"
}
}