From 9876dea4ae38eda7dbed81bb55d5f3442d6dfd4f Mon Sep 17 00:00:00 2001 From: Cosmin Tupangiu Date: Tue, 25 Feb 2025 17:11:07 +0100 Subject: [PATCH] cli: Add deploy command Signed-off-by: Cosmin Tupangiu --- cmd/planner/main.go | 1 + internal/cli/deploy.go | 240 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 internal/cli/deploy.go diff --git a/cmd/planner/main.go b/cmd/planner/main.go index c43ce12..b28c2d3 100644 --- a/cmd/planner/main.go +++ b/cmd/planner/main.go @@ -28,6 +28,7 @@ func NewPlannerCtlCommand() *cobra.Command { cmd.AddCommand(cli.NewCmdVersion()) cmd.AddCommand(cli.NewCmdCreate()) cmd.AddCommand(cli.NewCmdGenerate()) + cmd.AddCommand(cli.NewCmdDeploy()) return cmd } diff --git a/internal/cli/deploy.go b/internal/cli/deploy.go new file mode 100644 index 0000000..8d5d9eb --- /dev/null +++ b/internal/cli/deploy.go @@ -0,0 +1,240 @@ +package cli + +import ( + "context" + "fmt" + "html/template" + "os" + "path" + "strings" + + "github.com/google/uuid" + libvirt "github.com/libvirt/libvirt-go" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const libvirtDomainDefinitionTemplate = ` + + {{ .Name }} + 4096 + 2 + + + + + + + hvm + + + + + + + + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + + + + + + + + + +` + +const persistenceVolDefinitionTemplate = ` + + {{ .Name }} + 0 + 1 + + + + +` + +type domainParameters struct { + PersistenceFilePath string + ImagePath string + Network string + Name string + StoragePool string + Volume string +} + +type persistenceVolumeParameters struct { + Name string +} + +type DeployOptions struct { + GlobalOptions + + ImageFile string + Name string + NetworkName string + QemuUrl string + StoragePool string +} + +func DefaultDeployOptions() *DeployOptions { + return &DeployOptions{ + GlobalOptions: DefaultGlobalOptions(), + } +} + +func NewCmdDeploy() *cobra.Command { + o := DefaultDeployOptions() + cmd := &cobra.Command{ + Use: "deploy SOURCE_ID [FLAGS]", + Short: "Deploy an agent", + Example: "deploy -s ~/.ssh/some_key.pub --name agent_vm --network bridge", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := o.Complete(cmd, args); err != nil { + return err + } + if err := o.Validate(args); err != nil { + return err + } + return o.Run(cmd.Context(), args) + }, + SilenceUsage: true, + } + o.Bind(cmd.Flags()) + return cmd +} + +func (o *DeployOptions) Bind(fs *pflag.FlagSet) { + o.GlobalOptions.Bind(fs) + + fs.StringVarP(&o.ImageFile, "image-file", "f", o.ImageFile, "Path the iso image. If not set the image will be generated with default values.") + fs.StringVarP(&o.Name, "name", "", o.Name, "Name of the vm") + fs.StringVarP(&o.NetworkName, "network", "", "default", "Name of the network") + fs.StringVarP(&o.QemuUrl, "qemu-url", "p", "qemu:///session", "Url of qemu") + fs.StringVarP(&o.StoragePool, "storage-pool", "", "default", "Name of the storage pool") +} + +func (o *DeployOptions) Validate(args []string) error { + if _, err := uuid.Parse(args[0]); err != nil { + return fmt.Errorf("invalid source id: %s", err) + } + + if o.Name == "" { + // generate a vm like agent-123456 + o.Name = fmt.Sprintf("agent-%s", uuid.NewString()[:6]) + } + + return nil +} + +func (o *DeployOptions) Run(ctx context.Context, args []string) error { + if o.ImageFile == "" { + tmpFolder, err := os.MkdirTemp("", o.Name) + if err != nil { + return fmt.Errorf("failed to create temporary folder for image iso: %v", err) + } + o.ImageFile = path.Join(tmpFolder, "image.iso") + + generateO := DefaultGenerateOptions() + generateO.ImageType = "iso" + generateO.OutputImageFilePath = o.ImageFile + + if err := generateO.Validate(args); err != nil { + return err + } + + if err := generateO.Run(ctx, args); err != nil { + return err + } + } + + conn, err := libvirt.NewConnect(o.QemuUrl) + if err != nil { + return fmt.Errorf("failed to connect to hypervisor: %s", err) + } + defer func() { + conn.Close() + }() + + // try to find the storage pool + storagePool, err := conn.LookupStoragePoolByName(o.StoragePool) + if err != nil { + return fmt.Errorf("failed to find storage pool %s: %s", o.StoragePool, err) + } + + volumeDef, err := generateTemplate(persistenceVolDefinitionTemplate, persistenceVolumeParameters{ + Name: fmt.Sprintf("persistent-vol-%s", o.Name), + }) + if err != nil { + return fmt.Errorf("failed to generate volume domain definition: %s", err) + } + + // generate persistence volume + volume, err := storagePool.StorageVolCreateXML(volumeDef, libvirt.STORAGE_VOL_CREATE_PREALLOC_METADATA) + if err != nil { + return fmt.Errorf("failed to create persistence volume: %s", err) + } + + volumeName, err := volume.GetName() + if err != nil { + return fmt.Errorf("failed to get volume name: %s", err) + } + + // create domain defintion + domDefinition, err := generateTemplate(libvirtDomainDefinitionTemplate, domainParameters{ + ImagePath: o.ImageFile, + Network: o.NetworkName, + Name: o.Name, + Volume: volumeName, + StoragePool: o.StoragePool, + }) + if err != nil { + return fmt.Errorf("failed to generate libvirt domain definition: %s", err) + } + + domain, err := conn.DomainDefineXML(domDefinition) + if err != nil { + return fmt.Errorf("failed to define domain: %v", err) + } + defer func() { + _ = domain.Free() + }() + + // Start the domain + if err := domain.Create(); err != nil { + return fmt.Errorf("failed to create domain: %v", err) + } + + fmt.Printf("agent started: %s", o.Name) + + return nil +} + +func generateTemplate(defTemplate string, data any) (string, error) { + // create domain defintion + tmpl, err := template.New("template").Parse(defTemplate) + if err != nil { + return "", err + } + + var defBuilder strings.Builder + if err := tmpl.Execute(&defBuilder, data); err != nil { + return "", err + } + + return defBuilder.String(), nil +}