From 3e1dbfd0a8ffb7dfb69632f623a6f08342b0a7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Ma=C5=82ota-W=C3=B3jcik?= <59281144+outofforest@users.noreply.github.com> Date: Fri, 30 Jun 2023 10:04:15 +0200 Subject: [PATCH] Stop command for stopping VMs (#236) --- ...{osman-autostart.service => osman.service} | 3 +- build/osman.spec | 6 +- cmd/main.go | 1 + commands/start.go | 6 +- commands/stop.go | 54 +++++++++++ config/start.go | 2 +- config/stop.go | 28 ++++++ functions.go | 26 ++++- vm.go | 97 ++++++++++++++++++- 9 files changed, 211 insertions(+), 12 deletions(-) rename build/{osman-autostart.service => osman.service} (73%) create mode 100644 commands/stop.go create mode 100644 config/stop.go diff --git a/build/osman-autostart.service b/build/osman.service similarity index 73% rename from build/osman-autostart.service rename to build/osman.service index 0452520..9a2a0bd 100644 --- a/build/osman-autostart.service +++ b/build/osman.service @@ -1,5 +1,5 @@ [Unit] -Description=Osman starts VM tagged with auto +Description=Osman starts and stops VMs Requires=virtqemud.service virtnetworkd.service virtqemud-admin.socket After=virtqemud.service virtnetworkd.service virtqemud-admin.socket @@ -7,6 +7,7 @@ After=virtqemud.service virtnetworkd.service virtqemud-admin.socket Type=oneshot Environment="HOME=/root" ExecStart=/bin/sh -c "/usr/bin/osman drop --type=vm --all && /usr/bin/osman start :auto" +ExecStop=/bin/sh -c "/usr/bin/osman stop --all && /usr/bin/osman drop --type=vm --all" RemainAfterExit=true [Install] diff --git a/build/osman.spec b/build/osman.spec index 4de462a..fef0e12 100644 --- a/build/osman.spec +++ b/build/osman.spec @@ -5,7 +5,7 @@ Summary: Tool to manage OS images URL: https://github.com/outofforest/osman License: MIT -Requires: zfs +Requires: zfs libvirt %description Tool to manage OS images @@ -20,10 +20,10 @@ mkdir -p %{buildroot}/usr/bin mkdir -p %{buildroot}/usr/local/lib/systemd/system cp ./bin/osman-app %{buildroot}/usr/bin/osman -cp ./build/osman-autostart.service %{buildroot}/usr/local/lib/systemd/system/osman-autostart.service +cp ./build/osman.service %{buildroot}/usr/local/lib/systemd/system/osman.service %files /usr/bin/osman -/usr/local/lib/systemd/system/osman-autostart.service +/usr/local/lib/systemd/system/osman.service %post diff --git a/cmd/main.go b/cmd/main.go index e845fab..280dc97 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,6 +33,7 @@ func iocBuilder(c *ioc.Container) { c.SingletonNamed("build", commands.NewBuildCommand) c.SingletonNamed("mount", commands.NewMountCommand) c.SingletonNamed("start", commands.NewStartCommand) + c.SingletonNamed("stop", commands.NewStopCommand) c.SingletonNamed("list", commands.NewListCommand) c.SingletonNamed("drop", commands.NewDropCommand) c.SingletonNamed("tag", commands.NewTagCommand) diff --git a/commands/start.go b/commands/start.go index aa8b618..8e39a5f 100644 --- a/commands/start.go +++ b/commands/start.go @@ -22,9 +22,9 @@ func NewStartCommand(cmdF *CmdFactory) *cobra.Command { startF := &config.StartFactory{} cmd := &cobra.Command{ - Short: "Starts VM", - Args: cobra.RangeArgs(1, 2), - Use: "start [flags] image [name][:tag]", + Short: "Starts VMs", + Args: cobra.MinimumNArgs(1), + Use: "start [flags] [name][:tag]", RunE: cmdF.Cmd(func(c *ioc.Container) { c.Singleton(storageF.Config) c.Singleton(filterF.Config) diff --git a/commands/stop.go b/commands/stop.go new file mode 100644 index 0000000..dc85971 --- /dev/null +++ b/commands/stop.go @@ -0,0 +1,54 @@ +package commands + +import ( + "fmt" + + "github.com/outofforest/ioc/v2" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/outofforest/osman" + "github.com/outofforest/osman/config" + "github.com/outofforest/osman/infra/format" +) + +// NewStopCommand creates new stop command +func NewStopCommand(cmdF *CmdFactory) *cobra.Command { + var storageF *config.StorageFactory + var filterF *config.FilterFactory + var formatF *config.FormatFactory + stopF := &config.StopFactory{} + + cmd := &cobra.Command{ + Short: "Stops VMs", + Use: "stop [flags] [name][:tag]", + RunE: cmdF.Cmd(func(c *ioc.Container) { + c.Singleton(storageF.Config) + c.Singleton(filterF.Config) + c.Singleton(formatF.Config) + c.Singleton(stopF.Config) + }, func(c *ioc.Container, formatter format.Formatter) error { + var results []osman.Result + var err error + c.Call(osman.Stop, &results, &err) + if err != nil { + return err + } + err = nil + for _, r := range results { + if r.Result != nil { + err = errors.New("some stops failed") + break + } + } + fmt.Println(formatter.Format(results)) + return nil + }), + } + storageF = cmdF.AddStorageFlags(cmd) + filterF = cmdF.AddFilterFlags(cmd, []string{config.BuildTypeVM}) + formatF = cmdF.AddFormatFlags(cmd) + cmd.Flags().StringVar(&stopF.LibvirtAddr, "libvirt-addr", "unix:///var/run/libvirt/libvirt-sock", "Address libvirt listens on") + cmd.Flags().BoolVar(&stopF.All, "all", false, "It is required to set this flag to stop builds if no filters are provided") + return cmd +} diff --git a/config/start.go b/config/start.go index dc765b0..b563cce 100644 --- a/config/start.go +++ b/config/start.go @@ -22,7 +22,7 @@ type StartFactory struct { } // Config returns new start config -func (f *StartFactory) Config(args Args) Start { +func (f *StartFactory) Config() Start { config := Start{ XMLDir: f.XMLDir, VolumeDir: f.VolumeDir, diff --git a/config/stop.go b/config/stop.go new file mode 100644 index 0000000..ea6f10c --- /dev/null +++ b/config/stop.go @@ -0,0 +1,28 @@ +package config + +// StopFactory collects data for stop config +type StopFactory struct { + // If no filter is provided it is required to set this flag to stop builds + All bool + + // LibvirtAddr is the address libvirt listens on + LibvirtAddr string +} + +// Config returns new stop config +func (f *StopFactory) Config() Stop { + config := Stop{ + All: f.All, + LibvirtAddr: f.LibvirtAddr, + } + return config +} + +// Stop stores configuration for stop command +type Stop struct { + // If no filter is provided it is required to set this flag to stop builds + All bool + + // LibvirtAddr is the address libvirt listens on + LibvirtAddr string +} diff --git a/functions.go b/functions.go index 2c1a2ba..0f3d51d 100644 --- a/functions.go +++ b/functions.go @@ -72,7 +72,7 @@ func Mount(ctx context.Context, storage config.Storage, filtering config.Filter, return mounts, nil } -// Start starts VM +// Start starts VMs func Start(ctx context.Context, storage config.Storage, filtering config.Filter, start config.Start, s storage.Driver) ([]types.BuildInfo, error) { for i, key := range filtering.BuildKeys { if key.Tag == "" { @@ -153,6 +153,28 @@ func Start(ctx context.Context, storage config.Storage, filtering config.Filter, return vms, nil } +// Stop stops VMs +func Stop(ctx context.Context, filtering config.Filter, stop config.Stop, s storage.Driver) ([]Result, error) { + if !stop.All && len(filtering.BuildIDs) == 0 && len(filtering.BuildKeys) == 0 { + return nil, errors.New("neither filters are provided nor --all is set") + } + + builds, err := List(ctx, filtering, s) + if err != nil { + return nil, err + } + + l, err := libvirtConn(stop.LibvirtAddr) + if err != nil { + return nil, err + } + defer func() { + _ = l.Disconnect() + }() + + return stopVMs(ctx, l, builds) +} + // List lists builds func List(ctx context.Context, filtering config.Filter, s storage.Driver) ([]types.BuildInfo, error) { buildTypes := map[types.BuildType]bool{} @@ -203,7 +225,7 @@ type Result struct { // Drop drops builds func Drop(ctx context.Context, storage config.Storage, filtering config.Filter, drop config.Drop, s storage.Driver) ([]Result, error) { if !drop.All && len(filtering.BuildIDs) == 0 && len(filtering.BuildKeys) == 0 { - return nil, errors.New("neither filters are provided nor All is set") + return nil, errors.New("neither filters are provided nor --all is set") } builds, err := List(ctx, filtering, s) diff --git a/vm.go b/vm.go index b96a9b5..715110e 100644 --- a/vm.go +++ b/vm.go @@ -1070,6 +1070,9 @@ func undeployVMs(ctx context.Context, l *libvirt.Libvirt, vmsToDelete map[types. spawn(string(buildID), parallel.Continue, func(ctx context.Context) error { active, err := l.DomainIsActive(d) if err != nil { + if libvirt.IsNotFound(err) { + return nil + } return errors.WithStack(err) } @@ -1082,11 +1085,10 @@ func undeployVMs(ctx context.Context, l *libvirt.Libvirt, vmsToDelete map[types. mu.Lock() defer mu.Unlock() + results[buildID] = err if err == nil || libvirt.IsNotFound(err) { deletedVMs[buildID] = domainDocsByUUID[d.UUID] delete(domainDocsByUUID, d.UUID) - } else { - results[buildID] = err } return nil @@ -1106,6 +1108,97 @@ func undeployVMs(ctx context.Context, l *libvirt.Libvirt, vmsToDelete map[types. return results, nil } +func stopVMs(ctx context.Context, l *libvirt.Libvirt, vmsToStop []types.BuildInfo) ([]Result, error) { + domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive|libvirt.ConnectListDomainsInactive) + if err != nil { + return nil, errors.WithStack(err) + } + + domainsByBuildID := map[types.BuildID]libvirt.Domain{} + for _, d := range domains { + domainXML, err := l.DomainGetXMLDesc(d, 0) + if err != nil { + return nil, errors.WithStack(err) + } + + var domainDoc libvirtxml.Domain + if err := domainDoc.Unmarshal(domainXML); err != nil { + return nil, errors.WithStack(err) + } + + meta, err := parseMetadata(domainDoc) + if err != nil { + return nil, err + } + + if meta.BuildID != "" { + domainsByBuildID[meta.BuildID] = d + } + } + + mu := sync.Mutex{} + results := make([]Result, 0, len(vmsToStop)) + err = parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { + for _, build := range vmsToStop { + domain, exists := domainsByBuildID[build.BuildID] + if !exists { + continue + } + + buildID := build.BuildID + spawn(string(buildID), parallel.Continue, func(ctx context.Context) error { + active, err := l.DomainIsActive(domain) + if err != nil { + if libvirt.IsNotFound(err) { + return nil + } + return errors.WithStack(err) + } + if active == 0 { + return nil + } + + err = l.DomainShutdown(domain) + + mu.Lock() + defer mu.Unlock() + + results = append(results, Result{ + BuildID: buildID, + Result: err, + }) + + for { + select { + case <-ctx.Done(): + return errors.WithStack(ctx.Err()) + case <-time.After(time.Second): + } + + active, err := l.DomainIsActive(domain) + if err != nil { + if libvirt.IsNotFound(err) { + return nil + } + return errors.WithStack(err) + } + if active == 0 { + return nil + } + } + }) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return results, nil +} + type vmToDeploy struct { Image types.BuildInfo Mount types.BuildInfo