// Package systemd provides high-level functionality for interacting with systemd services package systemd import ( "context" "errors" "fmt" "path/filepath" "strings" "github.com/coreos/go-systemd/v22/dbus" ) const ( // Replace will start the unit and its dependencies, possibly replacing // already queued jobs that conflict with this. Replace Mode = "replace" // Fail will start the unit and its dependencies, but will fail if this // would change an already queued job. Fail Mode = "fail" // Isolate will start the unit in question and terminate all units that // aren't dependencies of it. Isolate Mode = "isolate" // IgnoreDependencies will start a unit but ignore all its dependencies. // This is not recommended. IgnoreDependencies Mode = "ignore-dependencies" // IgnoreRequirements will start a unit but only ignore the requirement // dependencies. This is not recommended. IgnoreRequirements Mode = "ignore-requirements" ) type Mode string //go:generate mockgen -destination=./mocks/connection.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Connection type Connection interface { StatusChecker Restarter Starter Stopper Disabler Closer } //go:generate mockgen -destination=./mocks/status_checker.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd StatusChecker type StatusChecker interface { ActiveState(ctx context.Context, service string) (string, error) SubState(ctx context.Context, service string) (string, error) Exists(ctx context.Context, service string) (bool, error) } //go:generate mockgen -destination=./mocks/starter.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Starter type Starter interface { Start(ctx context.Context, service string, mode Mode, failOnSkipped bool) error StartTransient(ctx context.Context, service string, mode Mode, properties []dbus.Property, failOnSkipped bool) error } //go:generate mockgen -destination=./mocks/restarter.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Restarter type Restarter interface { Restart(ctx context.Context, service string, mode Mode, failOnSkipped bool) error } //go:generate mockgen -destination=./mocks/stopper.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Stopper type Stopper interface { Stop(ctx context.Context, service string, mode Mode, failOnSkipped bool) error } //go:generate mockgen -destination=./mocks/disabler.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Disabler type Disabler interface { Disable(ctx context.Context, services []string) error } //go:generate mockgen -destination=./mocks/closer.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Closer type Closer interface { Close() } // connection is a wrapper around the connection to the systemd dbus endpoint which provides // additional high-level functionality type connection struct { *dbus.Conn } // NewConnection returns a new connection object containing a systemd connection. Callers should call Close() // when done with the connection func NewConnection(ctx context.Context) (Connection, error) { conn, err := dbus.NewSystemdConnectionContext(ctx) if err != nil { return nil, fmt.Errorf("failed to establish connection to systemd: %w", err) } return &connection{conn}, nil } // ActiveState retrieves the value of the ActiveState property of a service func (c *connection) ActiveState(ctx context.Context, service string) (string, error) { state, err := c.GetUnitPropertyContext(ctx, service, "ActiveState") if err != nil { return "", err } return strings.Trim(state.Value.String(), "\""), nil } // SubState retrieves the value of the SubState property of a service func (c *connection) SubState(ctx context.Context, service string) (string, error) { state, err := c.GetUnitPropertyContext(ctx, service, "SubState") if err != nil { return "", err } return strings.Trim(state.Value.String(), "\""), nil } // Start attempts to start a service. // // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements. // // failOnSkipped can be used to define whether or not to fail when the start was skipped due // to a start not being applicable to the services current state func (c *connection) Start(ctx context.Context, service string, mode Mode, failOnSkipped bool) error { resp := make(chan string) if _, err := c.StartUnitContext(ctx, service, string(mode), resp); err != nil { return err } jobResp := <-resp return handleResponse(jobResp, failOnSkipped) } // StartTransient attempts to start a transient service. // // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements. // // failOnSkipped can be used to define whether or not to fail when the start was skipped due // to a start not being applicable to the services current state func (c *connection) StartTransient(ctx context.Context, service string, mode Mode, properties []dbus.Property, failOnSkipped bool) error { resp := make(chan string) if _, err := c.StartTransientUnitContext(ctx, service, string(mode), properties, resp); err != nil { return err } jobResp := <-resp return handleResponse(jobResp, failOnSkipped) } // Restart attempts to restart a service. // // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements. // // failOnSkipped can be used to define whether or not to fail when the restart was skipped due // to a restart not being applicable to the services current state func (c *connection) Restart(ctx context.Context, service string, mode Mode, failOnSkipped bool) error { resp := make(chan string) if _, err := c.RestartUnitContext(ctx, service, string(mode), resp); err != nil { return err } jobResp := <-resp return handleResponse(jobResp, failOnSkipped) } // Stop attempts to stop a service. // // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements. // // failOnSkipped can be used to define whether or not to fail when the stop was skipped due // to a stop not being applicable to the services current state func (c *connection) Stop(ctx context.Context, service string, mode Mode, failOnSkipped bool) error { resp := make(chan string) if _, err := c.StopUnitContext(ctx, service, string(mode), resp); err != nil { return err } jobResp := <-resp return handleResponse(jobResp, failOnSkipped) } // handleResponse handles the response from a systemd job. If the response is "done", or // "skipped" (with failOnSkipped set to false) then the response is considered successful func handleResponse(resp string, failOnSkipped bool) error { switch resp { case "done": return nil case "skipped": if failOnSkipped { return errors.New("job skipped") } return nil default: return fmt.Errorf("error restarting service: %v", resp) } } // Exists checks if the given unit Exists func (c *connection) Exists(ctx context.Context, service string) (bool, error) { unitFiles, err := c.ListUnitFilesContext(ctx) if err != nil { return false, fmt.Errorf("failed to contact systemd bus: %w", err) } for _, unit := range unitFiles { if filepath.Base(unit.Path) == service { return true, nil } } return false, nil } // Disable takes a list of services and disables them by removing the symlinks to them. func (c *connection) Disable(ctx context.Context, services []string) error { _, err := c.DisableUnitFilesContext(ctx, services, false) return err }