...

Source file src/edge-infra.dev/pkg/sds/lib/dbus/systemd/systemd.go

Documentation: edge-infra.dev/pkg/sds/lib/dbus/systemd

     1  // Package systemd provides high-level functionality for interacting with systemd services
     2  package systemd
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/coreos/go-systemd/v22/dbus"
    12  )
    13  
    14  const (
    15  	// Replace will start the unit and its dependencies, possibly replacing
    16  	// already queued jobs that conflict with this.
    17  	Replace Mode = "replace"
    18  	// Fail will start the unit and its dependencies, but will fail if this
    19  	// would change an already queued job.
    20  	Fail Mode = "fail"
    21  	// Isolate will start the unit in question and terminate all units that
    22  	// aren't dependencies of it.
    23  	Isolate Mode = "isolate"
    24  	// IgnoreDependencies will start a unit but ignore all its dependencies.
    25  	// This is not recommended.
    26  	IgnoreDependencies Mode = "ignore-dependencies"
    27  	// IgnoreRequirements will start a unit but only ignore the requirement
    28  	// dependencies. This is not recommended.
    29  	IgnoreRequirements Mode = "ignore-requirements"
    30  )
    31  
    32  type Mode string
    33  
    34  //go:generate mockgen -destination=./mocks/connection.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Connection
    35  type Connection interface {
    36  	StatusChecker
    37  	Restarter
    38  	Starter
    39  	Stopper
    40  	Disabler
    41  	Closer
    42  }
    43  
    44  //go:generate mockgen -destination=./mocks/status_checker.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd StatusChecker
    45  type StatusChecker interface {
    46  	ActiveState(ctx context.Context, service string) (string, error)
    47  	SubState(ctx context.Context, service string) (string, error)
    48  	Exists(ctx context.Context, service string) (bool, error)
    49  }
    50  
    51  //go:generate mockgen -destination=./mocks/starter.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Starter
    52  type Starter interface {
    53  	Start(ctx context.Context, service string, mode Mode, failOnSkipped bool) error
    54  	StartTransient(ctx context.Context, service string, mode Mode, properties []dbus.Property, failOnSkipped bool) error
    55  }
    56  
    57  //go:generate mockgen -destination=./mocks/restarter.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Restarter
    58  type Restarter interface {
    59  	Restart(ctx context.Context, service string, mode Mode, failOnSkipped bool) error
    60  }
    61  
    62  //go:generate mockgen -destination=./mocks/stopper.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Stopper
    63  type Stopper interface {
    64  	Stop(ctx context.Context, service string, mode Mode, failOnSkipped bool) error
    65  }
    66  
    67  //go:generate mockgen -destination=./mocks/disabler.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Disabler
    68  type Disabler interface {
    69  	Disable(ctx context.Context, services []string) error
    70  }
    71  
    72  //go:generate mockgen -destination=./mocks/closer.go -package=mocks edge-infra.dev/pkg/sds/lib/dbus/systemd Closer
    73  type Closer interface {
    74  	Close()
    75  }
    76  
    77  // connection is a wrapper around the connection to the systemd dbus endpoint which provides
    78  // additional high-level functionality
    79  type connection struct {
    80  	*dbus.Conn
    81  }
    82  
    83  // NewConnection returns a new connection object containing a systemd connection. Callers should call Close()
    84  // when done with the connection
    85  func NewConnection(ctx context.Context) (Connection, error) {
    86  	conn, err := dbus.NewSystemdConnectionContext(ctx)
    87  	if err != nil {
    88  		return nil, fmt.Errorf("failed to establish connection to systemd: %w", err)
    89  	}
    90  	return &connection{conn}, nil
    91  }
    92  
    93  // ActiveState retrieves the value of the ActiveState property of a service
    94  func (c *connection) ActiveState(ctx context.Context, service string) (string, error) {
    95  	state, err := c.GetUnitPropertyContext(ctx, service, "ActiveState")
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  
   100  	return strings.Trim(state.Value.String(), "\""), nil
   101  }
   102  
   103  // SubState retrieves the value of the SubState property of a service
   104  func (c *connection) SubState(ctx context.Context, service string) (string, error) {
   105  	state, err := c.GetUnitPropertyContext(ctx, service, "SubState")
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  
   110  	return strings.Trim(state.Value.String(), "\""), nil
   111  }
   112  
   113  // Start attempts to start a service.
   114  //
   115  // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements.
   116  //
   117  // failOnSkipped can be used to define whether or not to fail when the start was skipped due
   118  // to a start not being applicable to the services current state
   119  func (c *connection) Start(ctx context.Context, service string, mode Mode, failOnSkipped bool) error {
   120  	resp := make(chan string)
   121  	if _, err := c.StartUnitContext(ctx, service, string(mode), resp); err != nil {
   122  		return err
   123  	}
   124  	jobResp := <-resp
   125  
   126  	return handleResponse(jobResp, failOnSkipped)
   127  }
   128  
   129  // StartTransient attempts to start a transient service.
   130  //
   131  // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements.
   132  //
   133  // failOnSkipped can be used to define whether or not to fail when the start was skipped due
   134  // to a start not being applicable to the services current state
   135  func (c *connection) StartTransient(ctx context.Context, service string, mode Mode, properties []dbus.Property, failOnSkipped bool) error {
   136  	resp := make(chan string)
   137  	if _, err := c.StartTransientUnitContext(ctx, service, string(mode), properties, resp); err != nil {
   138  		return err
   139  	}
   140  	jobResp := <-resp
   141  
   142  	return handleResponse(jobResp, failOnSkipped)
   143  }
   144  
   145  // Restart attempts to restart a service.
   146  //
   147  // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements.
   148  //
   149  // failOnSkipped can be used to define whether or not to fail when the restart was skipped due
   150  // to a restart not being applicable to the services current state
   151  func (c *connection) Restart(ctx context.Context, service string, mode Mode, failOnSkipped bool) error {
   152  	resp := make(chan string)
   153  	if _, err := c.RestartUnitContext(ctx, service, string(mode), resp); err != nil {
   154  		return err
   155  	}
   156  	jobResp := <-resp
   157  
   158  	return handleResponse(jobResp, failOnSkipped)
   159  }
   160  
   161  // Stop attempts to stop a service.
   162  //
   163  // The mode needs to be one of replace, fail, isolate, ignore-dependencies, ignore-requirements.
   164  //
   165  // failOnSkipped can be used to define whether or not to fail when the stop was skipped due
   166  // to a stop not being applicable to the services current state
   167  func (c *connection) Stop(ctx context.Context, service string, mode Mode, failOnSkipped bool) error {
   168  	resp := make(chan string)
   169  	if _, err := c.StopUnitContext(ctx, service, string(mode), resp); err != nil {
   170  		return err
   171  	}
   172  	jobResp := <-resp
   173  
   174  	return handleResponse(jobResp, failOnSkipped)
   175  }
   176  
   177  // handleResponse handles the response from a systemd job. If the response is "done", or
   178  // "skipped" (with failOnSkipped set to false) then the response is considered successful
   179  func handleResponse(resp string, failOnSkipped bool) error {
   180  	switch resp {
   181  	case "done":
   182  		return nil
   183  	case "skipped":
   184  		if failOnSkipped {
   185  			return errors.New("job skipped")
   186  		}
   187  		return nil
   188  	default:
   189  		return fmt.Errorf("error restarting service: %v", resp)
   190  	}
   191  }
   192  
   193  // Exists checks if the given unit Exists
   194  func (c *connection) Exists(ctx context.Context, service string) (bool, error) {
   195  	unitFiles, err := c.ListUnitFilesContext(ctx)
   196  	if err != nil {
   197  		return false, fmt.Errorf("failed to contact systemd bus: %w", err)
   198  	}
   199  
   200  	for _, unit := range unitFiles {
   201  		if filepath.Base(unit.Path) == service {
   202  			return true, nil
   203  		}
   204  	}
   205  
   206  	return false, nil
   207  }
   208  
   209  // Disable takes a list of services and disables them by removing the symlinks to them.
   210  func (c *connection) Disable(ctx context.Context, services []string) error {
   211  	_, err := c.DisableUnitFilesContext(ctx, services, false)
   212  	return err
   213  }
   214  

View as plain text