diff --git a/pkg/plugins/constants.go b/pkg/plugins/constants.go new file mode 100644 index 00000000000..3b9066df211 --- /dev/null +++ b/pkg/plugins/constants.go @@ -0,0 +1,11 @@ +package plugins + +import "time" + +const ( + // DefaultTimeout is the default timeout for gRPC operations + DefaultTimeout = 30 * time.Second + + // DefaultPluginDir is the default directory where plugins are stored + DefaultPluginDir = "/usr/local/lib/lima/plugins" +) \ No newline at end of file diff --git a/pkg/plugins/driver.proto b/pkg/plugins/driver.proto new file mode 100644 index 00000000000..9d1ed444b9c --- /dev/null +++ b/pkg/plugins/driver.proto @@ -0,0 +1,176 @@ +syntax = "proto3"; + +package plugins; + +option go_package = "github.com/lima-vm/lima/pkg/plugins"; + +service VMDriver { + rpc GetMetadata(GetMetadataRequest) returns (GetMetadataResponse); + rpc StartVM(StartVMRequest) returns (StartVMResponse); + rpc StopVM(StopVMRequest) returns (StopVMResponse); + rpc Initialize(InitializeRequest) returns (InitializeResponse); + rpc CreateDisk(CreateDiskRequest) returns (CreateDiskResponse); + rpc Validate(ValidateRequest) returns (ValidateResponse); + rpc Register(RegisterRequest) returns (RegisterResponse); + rpc Unregister(UnregisterRequest) returns (UnregisterResponse); + rpc ChangeDisplayPassword(ChangeDisplayPasswordRequest) returns (ChangeDisplayPasswordResponse); + rpc GetDisplayConnection(GetDisplayConnectionRequest) returns (GetDisplayConnectionResponse); + rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse); + rpc ApplySnapshot(ApplySnapshotRequest) returns (ApplySnapshotResponse); + rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse); + rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse); + rpc GetGuestAgentConnection(GetGuestAgentConnectionRequest) returns (GetGuestAgentConnectionResponse); +} + +// Response is a common response type used by helper functions +message Response { + bool success = 1; + string message = 2; +} + +message GetMetadataRequest {} + +message GetMetadataResponse { + string name = 1; + string version = 2; + string description = 3; + repeated string supported_vm_types = 4; +} + +message StartVMRequest { + string config = 1; // Serialized config +} + +message StartVMResponse { + bool success = 1; + string message = 2; + bool can_run_gui = 3; +} + +message StopVMRequest { + string instance_id = 1; +} + +message StopVMResponse { + bool success = 1; + string message = 2; +} + +message InitializeRequest { + string instance_id = 1; + string config = 2; +} + +message InitializeResponse { + bool success = 1; + string message = 2; +} + +message CreateDiskRequest { + string instance_id = 1; + string config = 2; +} + +message CreateDiskResponse { + bool success = 1; + string message = 2; +} + +message ValidateRequest { + string config = 1; +} + +message ValidateResponse { + bool success = 1; + string message = 2; +} + +message RegisterRequest { + string instance_id = 1; + string config = 2; +} + +message RegisterResponse { + bool success = 1; + string message = 2; +} + +message UnregisterRequest { + string instance_id = 1; +} + +message UnregisterResponse { + bool success = 1; + string message = 2; +} + +message ChangeDisplayPasswordRequest { + string instance_id = 1; + string password = 2; +} + +message ChangeDisplayPasswordResponse { + bool success = 1; + string message = 2; +} + +message GetDisplayConnectionRequest { + string instance_id = 1; +} + +message GetDisplayConnectionResponse { + bool success = 1; + string message = 2; + string connection = 3; +} + +message CreateSnapshotRequest { + string instance_id = 1; + string tag = 2; +} + +message CreateSnapshotResponse { + bool success = 1; + string message = 2; +} + +message ApplySnapshotRequest { + string instance_id = 1; + string tag = 2; +} + +message ApplySnapshotResponse { + bool success = 1; + string message = 2; +} + +message DeleteSnapshotRequest { + string instance_id = 1; + string tag = 2; +} + +message DeleteSnapshotResponse { + bool success = 1; + string message = 2; +} + +message ListSnapshotsRequest { + string instance_id = 1; +} + +message ListSnapshotsResponse { + bool success = 1; + string message = 2; + string snapshots = 3; +} + +message GetGuestAgentConnectionRequest { + string instance_id = 1; +} + +message GetGuestAgentConnectionResponse { + bool success = 1; + string message = 2; + bool forward_guest_agent = 3; + string connection_address = 4; +} diff --git a/pkg/plugins/framework/example/example.go b/pkg/plugins/framework/example/example.go new file mode 100644 index 00000000000..dbd8703defd --- /dev/null +++ b/pkg/plugins/framework/example/example.go @@ -0,0 +1,226 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/lima-vm/lima/pkg/plugins" + "github.com/lima-vm/lima/pkg/plugins/framework" +) + +// ExamplePlugin is an example VM driver plugin +type ExamplePlugin struct { + *framework.BasePluginServer + instances map[string]*Instance +} + +// Instance represents a running VM instance +type Instance struct { + ID string + Config *framework.Config + Status string +} + +// NewExamplePlugin creates a new example plugin +func NewExamplePlugin() *ExamplePlugin { + return &ExamplePlugin{ + BasePluginServer: framework.NewBasePluginServer( + "example", + "1.0.0", + "An example VM driver plugin", + []string{"example-vm"}, + ), + instances: make(map[string]*Instance), + } +} + +// Initialize implements the Initialize RPC +func (p *ExamplePlugin) Initialize(ctx context.Context, req *plugins.InitializeRequest) (*plugins.InitializeResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Validate config + if err := framework.ValidateConfig(config); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("invalid config: %v", err), + }, nil + } + + // Create instance directory + instanceDir := framework.GetInstanceDir(req.InstanceId) + if err := framework.EnsureDir(instanceDir); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to create instance directory: %v", err), + }, nil + } + + // Write config file + configPath := framework.GetInstanceConfigPath(req.InstanceId) + if err := framework.WriteConfig(config, configPath); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to write config: %v", err), + }, nil + } + + return &plugins.InitializeResponse{ + Success: true, + Message: "Instance initialized successfully", + }, nil +} + +// CreateDisk implements the CreateDisk RPC +func (p *ExamplePlugin) CreateDisk(ctx context.Context, req *plugins.CreateDiskRequest) (*plugins.CreateDiskResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + diskPath := framework.GetInstanceDiskPath(req.InstanceId) + if err := framework.CreateDiskImage(diskPath, *config.Disk); err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to create disk: %v", err), + }, nil + } + + return &plugins.CreateDiskResponse{ + Success: true, + Message: "Disk created successfully", + }, nil +} + +// StartVM implements the StartVM RPC +func (p *ExamplePlugin) StartVM(ctx context.Context, req *plugins.StartVMRequest) (*plugins.StartVMResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Create instance + instance := &Instance{ + ID: config.Name, + Config: config, + Status: "starting", + } + + // Store instance + p.instances[instance.ID] = instance + + // Example: Start a background process to simulate VM startup + go func() { + time.Sleep(2 * time.Second) // Simulate startup time + instance.Status = "running" + }() + + return &plugins.StartVMResponse{ + Success: true, + Message: "VM started successfully", + CanRunGui: false, + }, nil +} + +// StopVM implements the StopVM RPC +func (p *ExamplePlugin) StopVM(ctx context.Context, req *plugins.StopVMRequest) (*plugins.StopVMResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.StopVMResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Example: Stop the VM + instance.Status = "stopped" + delete(p.instances, req.InstanceId) + + return &plugins.StopVMResponse{ + Success: true, + Message: "VM stopped successfully", + }, nil +} + +// CreateSnapshot implements the CreateSnapshot RPC +func (p *ExamplePlugin) CreateSnapshot(ctx context.Context, req *plugins.CreateSnapshotRequest) (*plugins.CreateSnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + diskPath := framework.GetInstanceDiskPath(req.InstanceId) + snapshotPath := filepath.Join(framework.GetInstanceDir(req.InstanceId), fmt.Sprintf("snapshot-%s.qcow2", req.Tag)) + + if err := framework.CreateSnapshot(diskPath, snapshotPath); err != nil { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to create snapshot: %v", err), + }, nil + } + + return &plugins.CreateSnapshotResponse{ + Success: true, + Message: "Snapshot created successfully", + }, nil +} + +// DeleteSnapshot implements the DeleteSnapshot RPC +func (p *ExamplePlugin) DeleteSnapshot(ctx context.Context, req *plugins.DeleteSnapshotRequest) (*plugins.DeleteSnapshotResponse, error) { + snapshotPath := filepath.Join(framework.GetInstanceDir(req.InstanceId), fmt.Sprintf("snapshot-%s.qcow2", req.Tag)) + if err := framework.DeleteSnapshot(snapshotPath); err != nil { + return &plugins.DeleteSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to delete snapshot: %v", err), + }, nil + } + + return &plugins.DeleteSnapshotResponse{ + Success: true, + Message: "Snapshot deleted successfully", + }, nil +} + +// GetGuestAgentConnection implements the GetGuestAgentConnection RPC +func (p *ExamplePlugin) GetGuestAgentConnection(ctx context.Context, req *plugins.GetGuestAgentConnectionRequest) (*plugins.GetGuestAgentConnectionResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.GetGuestAgentConnectionResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + return &plugins.GetGuestAgentConnectionResponse{ + Success: true, + Message: "Guest agent connection info retrieved successfully", + ForwardGuestAgent: true, + ConnectionAddress: fmt.Sprintf("unix://%s", framework.GetInstanceSocketPath(req.InstanceId)), + }, nil +} + +func main() { + plugin := NewExamplePlugin() + socketPath := framework.GetPluginSocketPath("example") + if err := plugin.Start(socketPath); err != nil { + log.Fatalf("Failed to start plugin: %v", err) + } +} \ No newline at end of file diff --git a/pkg/plugins/framework/helpers.go b/pkg/plugins/framework/helpers.go new file mode 100644 index 00000000000..44b80754057 --- /dev/null +++ b/pkg/plugins/framework/helpers.go @@ -0,0 +1,174 @@ +package framework + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/lima-vm/lima/pkg/limayaml" +) + +// Config represents the VM configuration +type Config struct { + limayaml.LimaYAML +} + +// ParseConfig parses the serialized config into a Config struct +func ParseConfig(serializedConfig string) (*Config, error) { + var config Config + if err := json.Unmarshal([]byte(serializedConfig), &config); err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + return &config, nil +} + +// CreateResponse creates a success response +func CreateResponse(success bool, message string) *plugins.Response { + return &plugins.Response{ + Success: success, + Message: message, + } +} + +// CreateErrorResponse creates an error response +func CreateErrorResponse(err error) *plugins.Response { + return &plugins.Response{ + Success: false, + Message: err.Error(), + } +} + +// EnsureDir ensures a directory exists, creating it if necessary +func EnsureDir(path string) error { + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", path, err) + } + return nil +} + +// GetInstanceDir returns the instance directory path +func GetInstanceDir(instanceID string) string { + return filepath.Join(os.Getenv("HOME"), ".lima", instanceID) +} + +// GetInstanceConfigPath returns the instance config file path +func GetInstanceConfigPath(instanceID string) string { + return filepath.Join(GetInstanceDir(instanceID), "lima.yaml") +} + +// GetInstanceDiskPath returns the instance disk file path +func GetInstanceDiskPath(instanceID string) string { + return filepath.Join(GetInstanceDir(instanceID), "disk.img") +} + +// GetInstanceSocketPath returns the instance socket file path +func GetInstanceSocketPath(instanceID string) string { + return filepath.Join(GetInstanceDir(instanceID), "socket") +} + +// GetPluginSocketPath returns the plugin socket file path +func GetPluginSocketPath(pluginName string) string { + return filepath.Join("/tmp", fmt.Sprintf("lima-plugin-%s.sock", pluginName)) +} + +// CreateDiskImage creates a disk image with the given size +func CreateDiskImage(path string, sizeGB int) error { + cmd := exec.Command("qemu-img", "create", "-f", "qcow2", path, fmt.Sprintf("%dG", sizeGB)) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create disk image: %s: %w", string(output), err) + } + return nil +} + +// ResizeDiskImage resizes a disk image to the given size +func ResizeDiskImage(path string, sizeGB int) error { + cmd := exec.Command("qemu-img", "resize", path, fmt.Sprintf("%dG", sizeGB)) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to resize disk image: %s: %w", string(output), err) + } + return nil +} + +// CreateSnapshot creates a snapshot of a disk image +func CreateSnapshot(path, snapshotPath string) error { + cmd := exec.Command("qemu-img", "create", "-f", "qcow2", "-b", path, snapshotPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create snapshot: %s: %w", string(output), err) + } + return nil +} + +// DeleteSnapshot deletes a snapshot +func DeleteSnapshot(snapshotPath string) error { + if err := os.Remove(snapshotPath); err != nil { + return fmt.Errorf("failed to delete snapshot: %w", err) + } + return nil +} + +// WaitForSocket waits for a Unix socket to become available +func WaitForSocket(path string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for time.Now().Before(deadline) { + if _, err := os.Stat(path); err == nil { + return nil + } + <-ticker.C + } + return fmt.Errorf("timeout waiting for socket %s", path) +} + +// WriteConfig writes the config to a file +func WriteConfig(config *Config, path string) error { + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + return nil +} + +// ReadConfig reads the config from a file +func ReadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) + } + return ParseConfig(string(data)) +} + +// ValidateConfig validates the VM configuration +func ValidateConfig(config *Config) error { + if config.VMType == nil { + return fmt.Errorf("vmType is required") + } + if config.CPUs == nil { + return fmt.Errorf("cpus is required") + } + if config.Memory == nil { + return fmt.Errorf("memory is required") + } + if config.Disk == nil { + return fmt.Errorf("disk is required") + } + return nil +} + +// GetSSHConfig returns the SSH configuration for the VM +func GetSSHConfig(instanceID string) string { + return fmt.Sprintf(`Host lima-%s + HostName 127.0.0.1 + Port 60022 + User lima + IdentityFile ~/.lima/%s/id_ed25519 + StrictHostKeyChecking no + UserKnownHostsFile /dev/null`, instanceID, instanceID) +} \ No newline at end of file diff --git a/pkg/plugins/framework/server.go b/pkg/plugins/framework/server.go new file mode 100644 index 00000000000..2e0540e09a1 --- /dev/null +++ b/pkg/plugins/framework/server.go @@ -0,0 +1,150 @@ +package framework + +import ( + "context" + "fmt" + "net" + "os" + "os/signal" + "syscall" + + "github.com/lima-vm/lima/pkg/plugins" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// BasePluginServer provides a base implementation for VM driver plugins +type BasePluginServer struct { + plugins.UnimplementedVMDriverServer + metadata *plugins.GetMetadataResponse + server *grpc.Server +} + +// NewBasePluginServer creates a new base plugin server +func NewBasePluginServer(name, version, description string, supportedVMTypes []string) *BasePluginServer { + return &BasePluginServer{ + metadata: &plugins.GetMetadataResponse{ + Name: name, + Version: version, + Description: description, + SupportedVMTypes: supportedVMTypes, + }, + } +} + +// Start starts the plugin server on the given Unix socket path +func (s *BasePluginServer) Start(socketPath string) error { + // Remove existing socket file if it exists + if err := os.RemoveAll(socketPath); err != nil { + return fmt.Errorf("failed to remove existing socket: %w", err) + } + + // Create Unix domain socket listener + lis, err := net.Listen("unix", socketPath) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + + // Create gRPC server + s.server = grpc.NewServer() + plugins.RegisterVMDriverServer(s.server, s) + reflection.Register(s.server) + + // Handle graceful shutdown + go func() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + s.server.GracefulStop() + os.Exit(0) + }() + + // Start serving + if err := s.server.Serve(lis); err != nil { + return fmt.Errorf("failed to serve: %w", err) + } + + return nil +} + +// Stop gracefully stops the plugin server +func (s *BasePluginServer) Stop() { + if s.server != nil { + s.server.GracefulStop() + } +} + +// GetMetadata implements the GetMetadata RPC +func (s *BasePluginServer) GetMetadata(ctx context.Context, req *plugins.GetMetadataRequest) (*plugins.GetMetadataResponse, error) { + return s.metadata, nil +} + +// StartVM implements the StartVM RPC +func (s *BasePluginServer) StartVM(ctx context.Context, req *plugins.StartVMRequest) (*plugins.StartVMResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// StopVM implements the StopVM RPC +func (s *BasePluginServer) StopVM(ctx context.Context, req *plugins.StopVMRequest) (*plugins.StopVMResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// Initialize implements the Initialize RPC +func (s *BasePluginServer) Initialize(ctx context.Context, req *plugins.InitializeRequest) (*plugins.InitializeResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// CreateDisk implements the CreateDisk RPC +func (s *BasePluginServer) CreateDisk(ctx context.Context, req *plugins.CreateDiskRequest) (*plugins.CreateDiskResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// Validate implements the Validate RPC +func (s *BasePluginServer) Validate(ctx context.Context, req *plugins.ValidateRequest) (*plugins.ValidateResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// Register implements the Register RPC +func (s *BasePluginServer) Register(ctx context.Context, req *plugins.RegisterRequest) (*plugins.RegisterResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// Unregister implements the Unregister RPC +func (s *BasePluginServer) Unregister(ctx context.Context, req *plugins.UnregisterRequest) (*plugins.UnregisterResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// ChangeDisplayPassword implements the ChangeDisplayPassword RPC +func (s *BasePluginServer) ChangeDisplayPassword(ctx context.Context, req *plugins.ChangeDisplayPasswordRequest) (*plugins.ChangeDisplayPasswordResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// GetDisplayConnection implements the GetDisplayConnection RPC +func (s *BasePluginServer) GetDisplayConnection(ctx context.Context, req *plugins.GetDisplayConnectionRequest) (*plugins.GetDisplayConnectionResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// CreateSnapshot implements the CreateSnapshot RPC +func (s *BasePluginServer) CreateSnapshot(ctx context.Context, req *plugins.CreateSnapshotRequest) (*plugins.CreateSnapshotResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// ApplySnapshot implements the ApplySnapshot RPC +func (s *BasePluginServer) ApplySnapshot(ctx context.Context, req *plugins.ApplySnapshotRequest) (*plugins.ApplySnapshotResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// DeleteSnapshot implements the DeleteSnapshot RPC +func (s *BasePluginServer) DeleteSnapshot(ctx context.Context, req *plugins.DeleteSnapshotRequest) (*plugins.DeleteSnapshotResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// ListSnapshots implements the ListSnapshots RPC +func (s *BasePluginServer) ListSnapshots(ctx context.Context, req *plugins.ListSnapshotsRequest) (*plugins.ListSnapshotsResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// GetGuestAgentConnection implements the GetGuestAgentConnection RPC +func (s *BasePluginServer) GetGuestAgentConnection(ctx context.Context, req *plugins.GetGuestAgentConnectionRequest) (*plugins.GetGuestAgentConnectionResponse, error) { + return nil, fmt.Errorf("not implemented") +} \ No newline at end of file diff --git a/pkg/plugins/manager.go b/pkg/plugins/manager.go new file mode 100644 index 00000000000..6bbfb8c3da4 --- /dev/null +++ b/pkg/plugins/manager.go @@ -0,0 +1,159 @@ +package plugins + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// Manager handles plugin discovery and lifecycle management +type Manager struct { + mu sync.RWMutex + plugins map[string]*Plugin +} + +// Plugin represents a loaded VM driver plugin +type Plugin struct { + name string + path string + conn *grpc.ClientConn + client VMDriverClient + metadata *PluginMetadata +} + +// PluginMetadata contains information about the plugin +type PluginMetadata struct { + Name string + Version string + Description string + SupportedVMTypes []string +} + +// NewManager creates a new plugin manager +func NewManager() *Manager { + return &Manager{ + plugins: make(map[string]*Plugin), + } +} + +// LoadPlugin loads a plugin from the given path +func (m *Manager) LoadPlugin(ctx context.Context, path string) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Check if plugin is already loaded + if _, exists := m.plugins[path]; exists { + return fmt.Errorf("plugin already loaded: %s", path) + } + + // Create gRPC connection to the plugin + conn, err := grpc.Dial("unix://"+path, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + grpc.WithTimeout(defaultTimeout), + ) + if err != nil { + return fmt.Errorf("failed to connect to plugin: %w", err) + } + + // Create plugin client + client := NewVMDriverClient(conn) + + // Get plugin metadata + metadata, err := client.GetMetadata(ctx, &GetMetadataRequest{}) + if err != nil { + conn.Close() + return fmt.Errorf("failed to get plugin metadata: %w", err) + } + + plugin := &Plugin{ + name: metadata.Name, + path: path, + conn: conn, + client: client, + metadata: metadata, + } + + m.plugins[path] = plugin + return nil +} + +// UnloadPlugin unloads a plugin +func (m *Manager) UnloadPlugin(path string) error { + m.mu.Lock() + defer m.mu.Unlock() + + plugin, exists := m.plugins[path] + if !exists { + return fmt.Errorf("plugin not found: %s", path) + } + + if err := plugin.conn.Close(); err != nil { + return fmt.Errorf("failed to close plugin connection: %w", err) + } + + delete(m.plugins, path) + return nil +} + +// GetPlugin returns a plugin by name +func (m *Manager) GetPlugin(name string) (*Plugin, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + for _, plugin := range m.plugins { + if plugin.name == name { + return plugin, nil + } + } + return nil, fmt.Errorf("plugin not found: %s", name) +} + +// DiscoverPlugins discovers plugins in the plugin directory +func (m *Manager) DiscoverPlugins(ctx context.Context, pluginDir string) error { + entries, err := os.ReadDir(pluginDir) + if err != nil { + return fmt.Errorf("failed to read plugin directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + // Check if file is executable + path := filepath.Join(pluginDir, entry.Name()) + if _, err := os.Stat(path); err != nil { + continue + } + + // Try to load the plugin + if err := m.LoadPlugin(ctx, path); err != nil { + // Log error but continue with other plugins + fmt.Printf("Failed to load plugin %s: %v\n", path, err) + continue + } + } + + return nil +} + +// Close closes all plugin connections +func (m *Manager) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + + for _, plugin := range m.plugins { + if err := plugin.conn.Close(); err != nil { + return fmt.Errorf("failed to close plugin connection: %w", err) + } + } + + m.plugins = make(map[string]*Plugin) + return nil +} \ No newline at end of file diff --git a/pkg/plugins/qemu/qemu.go b/pkg/plugins/qemu/qemu.go new file mode 100644 index 00000000000..75e7b69e792 --- /dev/null +++ b/pkg/plugins/qemu/qemu.go @@ -0,0 +1,514 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/digitalocean/go-qemu/qmp" + "github.com/digitalocean/go-qemu/qmp/raw" + "github.com/lima-vm/lima/pkg/executil" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/plugins" + "github.com/lima-vm/lima/pkg/plugins/framework" + "github.com/lima-vm/lima/pkg/qemu" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" +) + +// QemuPlugin is the QEMU VM driver plugin +type QemuPlugin struct { + *framework.BasePluginServer + instances map[string]*Instance +} + +// Instance represents a running QEMU VM instance +type Instance struct { + ID string + Config *framework.Config + Status string + qCmd *exec.Cmd + qWaitCh chan error + vhostCmds []*exec.Cmd +} + +// NewQemuPlugin creates a new QEMU plugin +func NewQemuPlugin() *QemuPlugin { + return &QemuPlugin{ + BasePluginServer: framework.NewBasePluginServer( + "qemu", + "1.0.0", + "QEMU VM driver plugin for Lima", + []string{"qemu"}, + ), + instances: make(map[string]*Instance), + } +} + +// Initialize implements the Initialize RPC +func (p *QemuPlugin) Initialize(ctx context.Context, req *plugins.InitializeRequest) (*plugins.InitializeResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Validate config + if err := framework.ValidateConfig(config); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("invalid config: %v", err), + }, nil + } + + // Create instance directory + instanceDir := framework.GetInstanceDir(req.InstanceId) + if err := framework.EnsureDir(instanceDir); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to create instance directory: %v", err), + }, nil + } + + // Write config file + configPath := framework.GetInstanceConfigPath(req.InstanceId) + if err := framework.WriteConfig(config, configPath); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to write config: %v", err), + }, nil + } + + return &plugins.InitializeResponse{ + Success: true, + Message: "Instance initialized successfully", + }, nil +} + +// CreateDisk implements the CreateDisk RPC +func (p *QemuPlugin) CreateDisk(ctx context.Context, req *plugins.CreateDiskRequest) (*plugins.CreateDiskResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + qCfg := qemu.Config{ + Name: config.Name, + InstanceDir: framework.GetInstanceDir(req.InstanceId), + LimaYAML: &config.LimaYAML, + } + + if err := qemu.EnsureDisk(ctx, qCfg); err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to create disk: %v", err), + }, nil + } + + return &plugins.CreateDiskResponse{ + Success: true, + Message: "Disk created successfully", + }, nil +} + +// StartVM implements the StartVM RPC +func (p *QemuPlugin) StartVM(ctx context.Context, req *plugins.StartVMRequest) (*plugins.StartVMResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Create instance + instance := &Instance{ + ID: config.Name, + Config: config, + Status: "starting", + } + + // Store instance + p.instances[instance.ID] = instance + + // Start QEMU + qCfg := qemu.Config{ + Name: config.Name, + InstanceDir: framework.GetInstanceDir(instance.ID), + LimaYAML: &config.LimaYAML, + SSHLocalPort: 60022, // Default SSH port + SSHAddress: "127.0.0.1", + } + + // Start virtiofsd if needed + if *config.LimaYAML.MountType == limayaml.VIRTIOFS { + vhostExe, err := qemu.FindVirtiofsd(qExe) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to find virtiofsd: %v", err), + }, nil + } + + for i := range config.LimaYAML.Mounts { + args, err := qemu.VirtiofsdCmdline(qCfg, i) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to generate virtiofsd command line: %v", err), + }, nil + } + + vhostCmd := exec.CommandContext(ctx, vhostExe, args...) + if err := vhostCmd.Start(); err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to start virtiofsd: %v", err), + }, nil + } + + instance.vhostCmds = append(instance.vhostCmds, vhostCmd) + } + } + + qExe, qArgs, err := qemu.Cmdline(ctx, qCfg) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to generate QEMU command line: %v", err), + }, nil + } + + // Start QEMU process + qCmd := exec.CommandContext(ctx, qExe, qArgs...) + qCmd.SysProcAttr = executil.BackgroundSysProcAttr + + if err := qCmd.Start(); err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to start QEMU: %v", err), + }, nil + } + + instance.qCmd = qCmd + instance.qWaitCh = make(chan error) + go func() { + instance.qWaitCh <- qCmd.Wait() + }() + + // Wait for QEMU to start + if err := framework.WaitForSocket(framework.GetInstanceSocketPath(instance.ID), 30*time.Second); err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("timeout waiting for QEMU to start: %v", err), + }, nil + } + + instance.Status = "running" + + return &plugins.StartVMResponse{ + Success: true, + Message: "VM started successfully", + CanRunGui: true, + }, nil +} + +// StopVM implements the StopVM RPC +func (p *QemuPlugin) StopVM(ctx context.Context, req *plugins.StopVMRequest) (*plugins.StopVMResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.StopVMResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Stop QEMU gracefully + if err := p.shutdownQEMU(ctx, instance); err != nil { + return &plugins.StopVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to stop QEMU: %v", err), + }, nil + } + + instance.Status = "stopped" + delete(p.instances, req.InstanceId) + + return &plugins.StopVMResponse{ + Success: true, + Message: "VM stopped successfully", + }, nil +} + +// shutdownQEMU gracefully shuts down a QEMU instance +func (p *QemuPlugin) shutdownQEMU(ctx context.Context, instance *Instance) error { + // Connect to QMP socket + qmpSockPath := filepath.Join(framework.GetInstanceDir(instance.ID), "qmp.sock") + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to QMP socket: %v", err) + } + defer qmpClient.Disconnect() + + if err := qmpClient.Connect(); err != nil { + return fmt.Errorf("failed to connect to QMP: %v", err) + } + + // Send system_powerdown command + rawClient := raw.NewMonitor(qmpClient) + if err := rawClient.SystemPowerdown(); err != nil { + return fmt.Errorf("failed to send system_powerdown command: %v", err) + } + + // Wait for QEMU to exit + select { + case err := <-instance.qWaitCh: + return err + case <-ctx.Done(): + return ctx.Err() + case <-time.After(30 * time.Second): + // Force kill if graceful shutdown fails + if err := instance.qCmd.Process.Kill(); err != nil { + return fmt.Errorf("failed to kill QEMU process: %v", err) + } + return <-instance.qWaitCh + } +} + +// GetGuestAgentConnection implements the GetGuestAgentConnection RPC +func (p *QemuPlugin) GetGuestAgentConnection(ctx context.Context, req *plugins.GetGuestAgentConnectionRequest) (*plugins.GetGuestAgentConnectionResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.GetGuestAgentConnectionResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + return &plugins.GetGuestAgentConnectionResponse{ + Success: true, + Message: "Guest agent connection info retrieved successfully", + ForwardGuestAgent: true, + ConnectionAddress: fmt.Sprintf("unix://%s", framework.GetInstanceSocketPath(req.InstanceId)), + }, nil +} + +// CreateSnapshot implements the CreateSnapshot RPC +func (p *QemuPlugin) CreateSnapshot(ctx context.Context, req *plugins.CreateSnapshotRequest) (*plugins.CreateSnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qCfg := qemu.Config{ + Name: instance.ID, + InstanceDir: framework.GetInstanceDir(req.InstanceId), + LimaYAML: &instance.Config.LimaYAML, + } + + if err := qemu.Save(qCfg, instance.Status == "running", req.Tag); err != nil { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to create snapshot: %v", err), + }, nil + } + + return &plugins.CreateSnapshotResponse{ + Success: true, + Message: "Snapshot created successfully", + }, nil +} + +// DeleteSnapshot implements the DeleteSnapshot RPC +func (p *QemuPlugin) DeleteSnapshot(ctx context.Context, req *plugins.DeleteSnapshotRequest) (*plugins.DeleteSnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.DeleteSnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qCfg := qemu.Config{ + Name: instance.ID, + InstanceDir: framework.GetInstanceDir(req.InstanceId), + LimaYAML: &instance.Config.LimaYAML, + } + + if err := qemu.Del(qCfg, instance.Status == "running", req.Tag); err != nil { + return &plugins.DeleteSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to delete snapshot: %v", err), + }, nil + } + + return &plugins.DeleteSnapshotResponse{ + Success: true, + Message: "Snapshot deleted successfully", + }, nil +} + +// ApplySnapshot implements the ApplySnapshot RPC +func (p *QemuPlugin) ApplySnapshot(ctx context.Context, req *plugins.ApplySnapshotRequest) (*plugins.ApplySnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ApplySnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qCfg := qemu.Config{ + Name: instance.ID, + InstanceDir: framework.GetInstanceDir(req.InstanceId), + LimaYAML: &instance.Config.LimaYAML, + } + + if err := qemu.Load(qCfg, instance.Status == "running", req.Tag); err != nil { + return &plugins.ApplySnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to apply snapshot: %v", err), + }, nil + } + + return &plugins.ApplySnapshotResponse{ + Success: true, + Message: "Snapshot applied successfully", + }, nil +} + +// ListSnapshots implements the ListSnapshots RPC +func (p *QemuPlugin) ListSnapshots(ctx context.Context, req *plugins.ListSnapshotsRequest) (*plugins.ListSnapshotsResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ListSnapshotsResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qCfg := qemu.Config{ + Name: instance.ID, + InstanceDir: framework.GetInstanceDir(req.InstanceId), + LimaYAML: &instance.Config.LimaYAML, + } + + snapshots, err := qemu.List(qCfg, instance.Status == "running") + if err != nil { + return &plugins.ListSnapshotsResponse{ + Success: false, + Message: fmt.Sprintf("failed to list snapshots: %v", err), + }, nil + } + + return &plugins.ListSnapshotsResponse{ + Success: true, + Message: "Snapshots listed successfully", + Snapshots: snapshots, + }, nil +} + +// ChangeDisplayPassword implements the ChangeDisplayPassword RPC +func (p *QemuPlugin) ChangeDisplayPassword(ctx context.Context, req *plugins.ChangeDisplayPasswordRequest) (*plugins.ChangeDisplayPasswordResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qmpSockPath := filepath.Join(framework.GetInstanceDir(req.InstanceId), filenames.QMPSock) + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: fmt.Sprintf("failed to connect to QMP socket: %v", err), + }, nil + } + defer qmpClient.Disconnect() + + if err := qmpClient.Connect(); err != nil { + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: fmt.Sprintf("failed to connect to QMP: %v", err), + }, nil + } + + rawClient := raw.NewMonitor(qmpClient) + if err := rawClient.ChangeVNCPassword(req.Password); err != nil { + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: fmt.Sprintf("failed to change VNC password: %v", err), + }, nil + } + + return &plugins.ChangeDisplayPasswordResponse{ + Success: true, + Message: "Display password changed successfully", + }, nil +} + +// GetDisplayConnection implements the GetDisplayConnection RPC +func (p *QemuPlugin) GetDisplayConnection(ctx context.Context, req *plugins.GetDisplayConnectionRequest) (*plugins.GetDisplayConnectionResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + qmpSockPath := filepath.Join(framework.GetInstanceDir(req.InstanceId), filenames.QMPSock) + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: fmt.Sprintf("failed to connect to QMP socket: %v", err), + }, nil + } + defer qmpClient.Disconnect() + + if err := qmpClient.Connect(); err != nil { + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: fmt.Sprintf("failed to connect to QMP: %v", err), + }, nil + } + + rawClient := raw.NewMonitor(qmpClient) + info, err := rawClient.QueryVNC() + if err != nil { + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: fmt.Sprintf("failed to query VNC info: %v", err), + }, nil + } + + return &plugins.GetDisplayConnectionResponse{ + Success: true, + Message: "Display connection info retrieved successfully", + Connection: *info.Service, + }, nil +} + +func main() { + plugin := NewQemuPlugin() + socketPath := framework.GetPluginSocketPath("qemu") + if err := plugin.Start(socketPath); err != nil { + log.Fatalf("Failed to start plugin: %v", err) + } +} \ No newline at end of file diff --git a/pkg/plugins/vz/vz.go b/pkg/plugins/vz/vz.go new file mode 100644 index 00000000000..16c09b50024 --- /dev/null +++ b/pkg/plugins/vz/vz.go @@ -0,0 +1,442 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "sync" + "time" + + "github.com/Code-Hex/vz/v3" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/plugins" + "github.com/lima-vm/lima/pkg/plugins/framework" + "github.com/lima-vm/lima/pkg/vz" + "github.com/sirupsen/logrus" +) + +// VzPlugin is the VZ VM driver plugin +type VzPlugin struct { + *framework.BasePluginServer + instances map[string]*Instance +} + +// Instance represents a running VZ VM instance +type Instance struct { + ID string + Config *framework.Config + Status string + vm *vz.VirtualMachine + mu sync.Mutex +} + +// NewVzPlugin creates a new VZ plugin +func NewVzPlugin() *VzPlugin { + return &VzPlugin{ + BasePluginServer: framework.NewBasePluginServer( + "vz", + "1.0.0", + "VZ VM driver plugin for Lima", + []string{"vz"}, + ), + instances: make(map[string]*Instance), + } +} + +// Initialize implements the Initialize RPC +func (p *VzPlugin) Initialize(ctx context.Context, req *plugins.InitializeRequest) (*plugins.InitializeResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Validate config + if err := framework.ValidateConfig(config); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("invalid config: %v", err), + }, nil + } + + // Create instance directory + instanceDir := framework.GetInstanceDir(req.InstanceId) + if err := framework.EnsureDir(instanceDir); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to create instance directory: %v", err), + }, nil + } + + // Write config file + configPath := framework.GetInstanceConfigPath(req.InstanceId) + if err := framework.WriteConfig(config, configPath); err != nil { + return &plugins.InitializeResponse{ + Success: false, + Message: fmt.Sprintf("failed to write config: %v", err), + }, nil + } + + return &plugins.InitializeResponse{ + Success: true, + Message: "Instance initialized successfully", + }, nil +} + +// CreateDisk implements the CreateDisk RPC +func (p *VzPlugin) CreateDisk(ctx context.Context, req *plugins.CreateDiskRequest) (*plugins.CreateDiskResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Create base driver for disk creation + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(req.InstanceId), + Config: &config.LimaYAML, + }, + } + + if err := vz.EnsureDisk(ctx, baseDriver); err != nil { + return &plugins.CreateDiskResponse{ + Success: false, + Message: fmt.Sprintf("failed to create disk: %v", err), + }, nil + } + + return &plugins.CreateDiskResponse{ + Success: true, + Message: "Disk created successfully", + }, nil +} + +// StartVM implements the StartVM RPC +func (p *VzPlugin) StartVM(ctx context.Context, req *plugins.StartVMRequest) (*plugins.StartVMResponse, error) { + config, err := framework.ParseConfig(req.Config) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to parse config: %v", err), + }, nil + } + + // Create instance + instance := &Instance{ + ID: config.Name, + Config: config, + Status: "starting", + } + + // Store instance + p.instances[instance.ID] = instance + + // Create base driver for VM creation + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(instance.ID), + Config: &config.LimaYAML, + }, + } + + // Start VM + vm, errCh, err := vz.StartVM(ctx, baseDriver) + if err != nil { + return &plugins.StartVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to start VM: %v", err), + }, nil + } + + instance.vm = vm + + // Wait for VM to be ready + go func() { + select { + case err := <-errCh: + if err != nil { + logrus.Errorf("VM error: %v", err) + } + case <-ctx.Done(): + } + }() + + // Wait for VM to be running + timeout := time.After(30 * time.Second) + ticker := time.NewTicker(500 * time.Millisecond) + for { + select { + case <-timeout: + return &plugins.StartVMResponse{ + Success: false, + Message: "timeout waiting for VM to start", + }, nil + case <-ticker.C: + if instance.vm.State() == vz.VirtualMachineStateRunning { + instance.Status = "running" + return &plugins.StartVMResponse{ + Success: true, + Message: "VM started successfully", + CanRunGui: true, + }, nil + } + } + } +} + +// StopVM implements the StopVM RPC +func (p *VzPlugin) StopVM(ctx context.Context, req *plugins.StopVMRequest) (*plugins.StopVMResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.StopVMResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Stop VM gracefully + if err := p.shutdownVM(ctx, instance); err != nil { + return &plugins.StopVMResponse{ + Success: false, + Message: fmt.Sprintf("failed to stop VM: %v", err), + }, nil + } + + instance.Status = "stopped" + delete(p.instances, req.InstanceId) + + return &plugins.StopVMResponse{ + Success: true, + Message: "VM stopped successfully", + }, nil +} + +// shutdownVM gracefully shuts down a VZ VM instance +func (p *VzPlugin) shutdownVM(ctx context.Context, instance *Instance) error { + canStop := instance.vm.CanRequestStop() + if !canStop { + return errors.New("VM does not support graceful shutdown") + } + + _, err := instance.vm.RequestStop() + if err != nil { + return fmt.Errorf("failed to request VM stop: %v", err) + } + + timeout := time.After(5 * time.Second) + ticker := time.NewTicker(500 * time.Millisecond) + for { + select { + case <-timeout: + return errors.New("timeout waiting for VM to stop") + case <-ticker.C: + if instance.vm.State() == vz.VirtualMachineStateStopped { + return nil + } + } + } +} + +// GetGuestAgentConnection implements the GetGuestAgentConnection RPC +func (p *VzPlugin) GetGuestAgentConnection(ctx context.Context, req *plugins.GetGuestAgentConnectionRequest) (*plugins.GetGuestAgentConnectionResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.GetGuestAgentConnectionResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Get guest agent connection + conn, err := instance.vm.GuestAgentConn(ctx) + if err != nil { + return &plugins.GetGuestAgentConnectionResponse{ + Success: false, + Message: fmt.Sprintf("failed to get guest agent connection: %v", err), + }, nil + } + + return &plugins.GetGuestAgentConnectionResponse{ + Success: true, + Message: "Guest agent connection info retrieved successfully", + ForwardGuestAgent: true, + ConnectionAddress: fmt.Sprintf("unix://%s", framework.GetInstanceSocketPath(req.InstanceId)), + }, nil +} + +// CreateSnapshot implements the CreateSnapshot RPC +func (p *VzPlugin) CreateSnapshot(ctx context.Context, req *plugins.CreateSnapshotRequest) (*plugins.CreateSnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Create base driver for snapshot creation + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(req.InstanceId), + Config: &instance.Config.LimaYAML, + }, + } + + if err := vz.CreateSnapshot(baseDriver, req.Tag); err != nil { + return &plugins.CreateSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to create snapshot: %v", err), + }, nil + } + + return &plugins.CreateSnapshotResponse{ + Success: true, + Message: "Snapshot created successfully", + }, nil +} + +// DeleteSnapshot implements the DeleteSnapshot RPC +func (p *VzPlugin) DeleteSnapshot(ctx context.Context, req *plugins.DeleteSnapshotRequest) (*plugins.DeleteSnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.DeleteSnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Create base driver for snapshot deletion + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(req.InstanceId), + Config: &instance.Config.LimaYAML, + }, + } + + if err := vz.DeleteSnapshot(baseDriver, req.Tag); err != nil { + return &plugins.DeleteSnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to delete snapshot: %v", err), + }, nil + } + + return &plugins.DeleteSnapshotResponse{ + Success: true, + Message: "Snapshot deleted successfully", + }, nil +} + +// ApplySnapshot implements the ApplySnapshot RPC +func (p *VzPlugin) ApplySnapshot(ctx context.Context, req *plugins.ApplySnapshotRequest) (*plugins.ApplySnapshotResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ApplySnapshotResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Create base driver for snapshot application + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(req.InstanceId), + Config: &instance.Config.LimaYAML, + }, + } + + if err := vz.ApplySnapshot(baseDriver, req.Tag); err != nil { + return &plugins.ApplySnapshotResponse{ + Success: false, + Message: fmt.Sprintf("failed to apply snapshot: %v", err), + }, nil + } + + return &plugins.ApplySnapshotResponse{ + Success: true, + Message: "Snapshot applied successfully", + }, nil +} + +// ListSnapshots implements the ListSnapshots RPC +func (p *VzPlugin) ListSnapshots(ctx context.Context, req *plugins.ListSnapshotsRequest) (*plugins.ListSnapshotsResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ListSnapshotsResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // Create base driver for snapshot listing + baseDriver := &driver.BaseDriver{ + Instance: &store.Instance{ + Dir: framework.GetInstanceDir(req.InstanceId), + Config: &instance.Config.LimaYAML, + }, + } + + snapshots, err := vz.ListSnapshots(baseDriver) + if err != nil { + return &plugins.ListSnapshotsResponse{ + Success: false, + Message: fmt.Sprintf("failed to list snapshots: %v", err), + }, nil + } + + return &plugins.ListSnapshotsResponse{ + Success: true, + Message: "Snapshots listed successfully", + Snapshots: snapshots, + }, nil +} + +// ChangeDisplayPassword implements the ChangeDisplayPassword RPC +func (p *VzPlugin) ChangeDisplayPassword(ctx context.Context, req *plugins.ChangeDisplayPasswordRequest) (*plugins.ChangeDisplayPasswordResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // VZ driver does not support display password changes + return &plugins.ChangeDisplayPasswordResponse{ + Success: false, + Message: "Display password changes are not supported by the VZ driver", + }, nil +} + +// GetDisplayConnection implements the GetDisplayConnection RPC +func (p *VzPlugin) GetDisplayConnection(ctx context.Context, req *plugins.GetDisplayConnectionRequest) (*plugins.GetDisplayConnectionResponse, error) { + instance, exists := p.instances[req.InstanceId] + if !exists { + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: "VM not found", + }, nil + } + + // VZ driver does not support display connections + return &plugins.GetDisplayConnectionResponse{ + Success: false, + Message: "Display connections are not supported by the VZ driver", + }, nil +} + +func main() { + plugin := NewVzPlugin() + socketPath := framework.GetPluginSocketPath("vz") + if err := plugin.Start(socketPath); err != nil { + log.Fatalf("Failed to start plugin: %v", err) + } +} \ No newline at end of file