Add podman machine events

Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
Jhon Honce 2022-04-04 13:04:40 -07:00
parent 1e0c50df38
commit 8da5f3f733
16 changed files with 266 additions and 50 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -145,11 +146,14 @@ func initMachine(cmd *cobra.Command, args []string) error {
// Finished = *, err != nil - Exit with an error message // Finished = *, err != nil - Exit with an error message
return err return err
} }
newMachineEvent(events.Init, events.Event{Name: initOpts.Name})
fmt.Println("Machine init complete") fmt.Println("Machine init complete")
if now { if now {
err = vm.Start(initOpts.Name, machine.StartOptions{}) err = vm.Start(initOpts.Name, machine.StartOptions{})
if err == nil { if err == nil {
fmt.Printf("Machine %q started successfully\n", initOpts.Name) fmt.Printf("Machine %q started successfully\n", initOpts.Name)
newMachineEvent(events.Start, events.Event{Name: initOpts.Name})
} }
} else { } else {
extra := "" extra := ""

View File

@ -4,7 +4,6 @@
package machine package machine
import ( import (
"encoding/json"
"os" "os"
"github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/common"

View File

@ -4,7 +4,6 @@
package machine package machine
import ( import (
"encoding/json"
"os" "os"
"sort" "sort"
"strconv" "strconv"

View File

@ -4,25 +4,39 @@
package machine package machine
import ( import (
"errors"
"net"
"os"
"path/filepath"
"regexp"
"strings" "strings"
"sync"
"time"
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/validate" "github.com/containers/podman/v4/cmd/podman/validate"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/util"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
noOp = func(cmd *cobra.Command, args []string) error { // Pull in configured json library
return nil json = registry.JSONLibrary()
}
sockPaths []string // Paths to unix domain sockets for publishing
openEventSock sync.Once // Singleton support for opening sockets as needed
sockets []net.Conn // Opened sockets, if any
// Command: podman _machine_ // Command: podman _machine_
machineCmd = &cobra.Command{ machineCmd = &cobra.Command{
Use: "machine", Use: "machine",
Short: "Manage a virtual machine", Short: "Manage a virtual machine",
Long: "Manage a virtual machine. Virtual machines are used to run Podman.", Long: "Manage a virtual machine. Virtual machines are used to run Podman.",
PersistentPreRunE: noOp, PersistentPreRunE: initMachineEvents,
PersistentPostRunE: noOp, PersistentPostRunE: closeMachineEvents,
RunE: validate.SubCommandExists, RunE: validate.SubCommandExists,
} }
) )
@ -64,3 +78,111 @@ func getMachines(toComplete string) ([]string, cobra.ShellCompDirective) {
} }
return suggestions, cobra.ShellCompDirectiveNoFileComp return suggestions, cobra.ShellCompDirectiveNoFileComp
} }
func initMachineEvents(cmd *cobra.Command, _ []string) error {
logrus.Debugf("Called machine %s.PersistentPreRunE(%s)", cmd.Name(), strings.Join(os.Args, " "))
sockPaths, err := resolveEventSock()
if err != nil {
return err
}
// No sockets found, so no need to publish events...
if len(sockPaths) == 0 {
return nil
}
for _, path := range sockPaths {
conn, err := (&net.Dialer{}).DialContext(registry.Context(), "unix", path)
if err != nil {
logrus.Warnf("Failed to open event socket %q: %v", path, err)
continue
}
logrus.Debugf("Machine event socket %q found", path)
sockets = append(sockets, conn)
}
return nil
}
func resolveEventSock() ([]string, error) {
// Used mostly for testing
if sock, found := os.LookupEnv("PODMAN_MACHINE_EVENTS_SOCK"); found {
return []string{sock}, nil
}
xdg, err := util.GetRuntimeDir()
if err != nil {
logrus.Warnf("Failed to get runtime dir, machine events will not be published: %s", err)
return nil, nil
}
re := regexp.MustCompile(`machine_events.*\.sock`)
sockPaths := make([]string, 0)
fn := func(path string, info os.DirEntry, err error) error {
switch {
case err != nil:
return err
case info.IsDir():
return nil
case info.Type() != os.ModeSocket:
return nil
case !re.MatchString(info.Name()):
return nil
}
logrus.Debugf("Machine events will be published on: %q", path)
sockPaths = append(sockPaths, path)
return nil
}
if err := filepath.WalkDir(filepath.Join(xdg, "podman"), fn); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
return sockPaths, nil
}
func newMachineEvent(status events.Status, event events.Event) {
openEventSock.Do(func() {
// No sockets where found, so no need to publish events...
if len(sockPaths) == 0 {
return
}
for _, path := range sockPaths {
conn, err := (&net.Dialer{}).DialContext(registry.Context(), "unix", path)
if err != nil {
logrus.Warnf("Failed to open event socket %q: %v", path, err)
continue
}
logrus.Debugf("Machine event socket %q found", path)
sockets = append(sockets, conn)
}
})
event.Status = status
event.Time = time.Now()
event.Type = events.Machine
payload, err := json.Marshal(event)
if err != nil {
logrus.Errorf("Unable to format machine event: %q", err)
return
}
for _, sock := range sockets {
if _, err := sock.Write(payload); err != nil {
logrus.Errorf("Unable to write machine event: %q", err)
}
}
}
func closeMachineEvents(cmd *cobra.Command, _ []string) error {
logrus.Debugf("Called machine %s.PersistentPostRunE(%s)", cmd.Name(), strings.Join(os.Args, " "))
for _, sock := range sockets {
_ = sock.Close()
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -50,7 +51,7 @@ func init() {
flags.BoolVar(&destroyOptions.SaveImage, imageFlagName, false, "Do not delete the image file") flags.BoolVar(&destroyOptions.SaveImage, imageFlagName, false, "Do not delete the image file")
} }
func rm(cmd *cobra.Command, args []string) error { func rm(_ *cobra.Command, args []string) error {
var ( var (
err error err error
vm machine.VM vm machine.VM
@ -83,5 +84,10 @@ func rm(cmd *cobra.Command, args []string) error {
return nil return nil
} }
} }
return remove() err = remove()
if err != nil {
return err
}
newMachineEvent(events.Remove, events.Event{Name: vmName})
return nil
} }

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -31,7 +32,7 @@ func init() {
}) })
} }
func start(cmd *cobra.Command, args []string) error { func start(_ *cobra.Command, args []string) error {
var ( var (
err error err error
vm machine.VM vm machine.VM
@ -62,5 +63,6 @@ func start(cmd *cobra.Command, args []string) error {
return err return err
} }
fmt.Printf("Machine %q started successfully\n", vmName) fmt.Printf("Machine %q started successfully\n", vmName)
newMachineEvent(events.Start, events.Event{Name: vmName})
return nil return nil
} }

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -49,5 +50,6 @@ func stop(cmd *cobra.Command, args []string) error {
return err return err
} }
fmt.Printf("Machine %q stopped successfully\n", vmName) fmt.Printf("Machine %q stopped successfully\n", vmName)
newMachineEvent(events.Stop, events.Event{Name: vmName})
return nil return nil
} }

View File

@ -18,6 +18,7 @@ import (
_ "github.com/containers/podman/v4/cmd/podman/secrets" _ "github.com/containers/podman/v4/cmd/podman/secrets"
_ "github.com/containers/podman/v4/cmd/podman/system" _ "github.com/containers/podman/v4/cmd/podman/system"
_ "github.com/containers/podman/v4/cmd/podman/system/connection" _ "github.com/containers/podman/v4/cmd/podman/system/connection"
"github.com/containers/podman/v4/cmd/podman/validate"
_ "github.com/containers/podman/v4/cmd/podman/volumes" _ "github.com/containers/podman/v4/cmd/podman/volumes"
"github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/rootless"
@ -64,8 +65,8 @@ func parseCommands() *cobra.Command {
c.Command.Hidden = true c.Command.Hidden = true
// overwrite persistent pre/post function to skip setup // overwrite persistent pre/post function to skip setup
c.Command.PersistentPostRunE = noop c.Command.PersistentPostRunE = validate.NoOp
c.Command.PersistentPreRunE = noop c.Command.PersistentPreRunE = validate.NoOp
addCommand(c) addCommand(c)
continue continue
} }
@ -120,7 +121,3 @@ func addCommand(c registry.CliCommand) {
c.Command.SetUsageTemplate(usageTemplate) c.Command.SetUsageTemplate(usageTemplate)
c.Command.DisableFlagsInUseLine = true c.Command.DisableFlagsInUseLine = true
} }
func noop(cmd *cobra.Command, args []string) error {
return nil
}

View File

@ -7,18 +7,15 @@ import (
) )
var ( var (
// Skip creating engines since this command will obtain connection information to said engines // ConnectionCmd skips creating engines (PersistentPreRunE/PersistentPostRunE are No-Op's) since
noOp = func(cmd *cobra.Command, args []string) error { // sub-commands will obtain connection information to said engines
return nil
}
ConnectionCmd = &cobra.Command{ ConnectionCmd = &cobra.Command{
Use: "connection", Use: "connection",
Short: "Manage remote ssh destinations", Short: "Manage remote API service destinations",
Long: `Manage ssh destination information in podman configuration`, Long: `Manage remote API service destination information in podman configuration`,
PersistentPreRunE: noOp, PersistentPreRunE: validate.NoOp,
RunE: validate.SubCommandExists, RunE: validate.SubCommandExists,
PersistentPostRunE: noOp, PersistentPostRunE: validate.NoOp,
TraverseChildren: false, TraverseChildren: false,
} }
) )

View File

@ -0,0 +1,9 @@
package validate
import (
"github.com/spf13/cobra"
)
func NoOp(_ *cobra.Command, _ []string) error {
return nil
}

View File

@ -17,6 +17,8 @@ const (
Journald EventerType = iota Journald EventerType = iota
// Null is a no-op events logger. It does not read or write events. // Null is a no-op events logger. It does not read or write events.
Null EventerType = iota Null EventerType = iota
// Memory indicates the event logger will hold events in memory
Memory EventerType = iota
) )
// Event describes the attributes of a libpod event // Event describes the attributes of a libpod event
@ -55,7 +57,7 @@ type Details struct {
// EventerOptions describe options that need to be passed to create // EventerOptions describe options that need to be passed to create
// an eventer // an eventer
type EventerOptions struct { type EventerOptions struct {
// EventerType describes whether to use journald or a file // EventerType describes whether to use journald, file or memory
EventerType string EventerType string
// LogFilePath is the path to where the log file should reside if using // LogFilePath is the path to where the log file should reside if using
// the file logger // the file logger
@ -110,6 +112,8 @@ const (
System Type = "system" System Type = "system"
// Volume - event is related to volumes // Volume - event is related to volumes
Volume Type = "volume" Volume Type = "volume"
// Machine - event is related to machine VM's
Machine Type = "machine"
// Attach ... // Attach ...
Attach Status = "attach" Attach Status = "attach"

View File

@ -20,6 +20,8 @@ func (et EventerType) String() string {
return "file" return "file"
case Journald: case Journald:
return "journald" return "journald"
case Memory:
return "memory"
case Null: case Null:
return "none" return "none"
default: default:
@ -34,6 +36,8 @@ func IsValidEventer(eventer string) bool {
return true return true
case Journald.String(): case Journald.String():
return true return true
case Memory.String():
return true
case Null.String(): case Null.String():
return true return true
default: default:
@ -41,7 +45,7 @@ func IsValidEventer(eventer string) bool {
} }
} }
// NewEvent creates a event struct and populates with // NewEvent creates an event struct and populates with
// the given status and time. // the given status and time.
func NewEvent(status Status) Event { func NewEvent(status Status) Event {
return Event{ return Event{
@ -63,7 +67,7 @@ func (e *Event) ToJSONString() (string, error) {
return string(b), err return string(b), err
} }
// ToHumanReadable returns human readable event as a formatted string // ToHumanReadable returns human-readable event as a formatted string
func (e *Event) ToHumanReadable(truncate bool) string { func (e *Event) ToHumanReadable(truncate bool) string {
var humanFormat string var humanFormat string
id := e.ID id := e.ID
@ -90,7 +94,7 @@ func (e *Event) ToHumanReadable(truncate bool) string {
} else { } else {
humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status) humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status)
} }
case Volume: case Volume, Machine:
humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name) humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name)
} }
return humanFormat return humanFormat
@ -99,19 +103,19 @@ func (e *Event) ToHumanReadable(truncate bool) string {
// NewEventFromString takes stringified json and converts // NewEventFromString takes stringified json and converts
// it to an event // it to an event
func newEventFromJSONString(event string) (*Event, error) { func newEventFromJSONString(event string) (*Event, error) {
e := Event{} e := new(Event)
if err := json.Unmarshal([]byte(event), &e); err != nil { if err := json.Unmarshal([]byte(event), e); err != nil {
return nil, err return nil, err
} }
return &e, nil return e, nil
} }
// ToString converts a Type to a string // String converts a Type to a string
func (t Type) String() string { func (t Type) String() string {
return string(t) return string(t)
} }
// ToString converts a status to a string // String converts a status to a string
func (s Status) String() string { func (s Status) String() string {
return string(s) return string(s)
} }
@ -123,6 +127,8 @@ func StringToType(name string) (Type, error) {
return Container, nil return Container, nil
case Image.String(): case Image.String():
return Image, nil return Image, nil
case Machine.String():
return Machine, nil
case Network.String(): case Network.String():
return Network, nil return Network, nil
case Pod.String(): case Pod.String():

View File

@ -21,6 +21,8 @@ func NewEventer(options EventerOptions) (Eventer, error) {
return EventLogFile{options}, nil return EventLogFile{options}, nil
case strings.ToUpper(Null.String()): case strings.ToUpper(Null.String()):
return NewNullEventer(), nil return NewNullEventer(), nil
case strings.ToUpper(Memory.String()):
return NewMemoryEventer(), nil
default: default:
return nil, errors.Errorf("unknown event logger type: %s", strings.ToUpper(options.EventerType)) return nil, errors.Errorf("unknown event logger type: %s", strings.ToUpper(options.EventerType))
} }

49
libpod/events/memory.go Normal file
View File

@ -0,0 +1,49 @@
package events
import (
"context"
)
// EventMemory is the structure for event writing to a channel. It contains the eventer
// options and the event itself. Methods for reading and writing are also defined from it.
type EventMemory struct {
options EventerOptions
elements chan *Event
}
// Write event to memory queue
func (e EventMemory) Write(event Event) (err error) {
e.elements <- &event
return
}
// Read event(s) from memory queue
func (e EventMemory) Read(ctx context.Context, options ReadOptions) (err error) {
select {
case <-ctx.Done():
return
default:
}
select {
case event := <-e.elements:
options.EventChannel <- event
default:
}
return nil
}
// String returns eventer type
func (e EventMemory) String() string {
return e.options.EventerType
}
// NewMemoryEventer returns configured MemoryEventer
func NewMemoryEventer() Eventer {
return EventMemory{
options: EventerOptions{
EventerType: Memory.String(),
},
elements: make(chan *Event, 100),
}
}

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net" "net"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -1063,3 +1064,36 @@ func digShort(container, lookupName string, matchNames []string, p *PodmanTestIn
} }
Fail("dns is not responding") Fail("dns is not responding")
} }
// WaitForFile to be created in defaultWaitTimeout seconds, returns false if file not created
func WaitForFile(path string) (err error) {
until := time.Now().Add(time.Duration(defaultWaitTimeout) * time.Second)
for i := 1; time.Now().Before(until); i++ {
_, err = os.Stat(path)
switch {
case err == nil:
return nil
case errors.Is(err, os.ErrNotExist):
time.Sleep(time.Duration(i) * time.Second)
default:
return err
}
}
return err
}
// WaitForService blocks, waiting for some service listening on given host:port
func WaitForService(address url.URL) {
// Wait for podman to be ready
var conn net.Conn
var err error
for i := 1; i <= 5; i++ {
conn, err = net.Dial("tcp", address.Host)
if err != nil {
// Podman not available yet...
time.Sleep(time.Duration(i) * time.Second)
}
}
Expect(err).ShouldNot(HaveOccurred())
conn.Close()
}

View File

@ -122,22 +122,6 @@ var _ = Describe("podman system service", func() {
}) })
}) })
// WaitForService blocks, waiting for some service listening on given host:port
func WaitForService(address url.URL) {
// Wait for podman to be ready
var conn net.Conn
var err error
for i := 1; i <= 5; i++ {
conn, err = net.Dial("tcp", address.Host)
if err != nil {
// Podman not available yet...
time.Sleep(time.Duration(i) * time.Second)
}
}
Expect(err).ShouldNot(HaveOccurred())
conn.Close()
}
// randomPort leans on the go net library to find an available port... // randomPort leans on the go net library to find an available port...
func randomPort() string { func randomPort() string {
port, err := utils.GetRandomPort() port, err := utils.GetRandomPort()