Permalink
Fetching contributors…
Cannot retrieve contributors at this time
460 lines (386 sloc) 11.9 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package systemd
import (
"errors"
"fmt"
"io"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
)
var (
// the output of "show" must match this for Stop to be done:
isStopDone = regexp.MustCompile(`(?m)\AActiveState=(?:failed|inactive)$`).Match
// how much time should Stop wait between calls to show
stopCheckDelay = 250 * time.Millisecond
// how much time should Stop wait between notifying the user of the waiting
stopNotifyDelay = 20 * time.Second
)
// systemctlCmd calls systemctl with the given args, returning its standard output (and wrapped error)
var systemctlCmd = func(args ...string) ([]byte, error) {
bs, err := exec.Command("systemctl", args...).CombinedOutput()
if err != nil {
exitCode, _ := osutil.ExitCode(err)
return nil, &Error{cmd: args, exitCode: exitCode, msg: bs}
}
return bs, nil
}
// MockSystemctl is called from the commands to actually call out to
// systemctl. It's exported so it can be overridden by testing.
func MockSystemctl(f func(args ...string) ([]byte, error)) func() {
oldSystemctlCmd := systemctlCmd
systemctlCmd = f
return func() {
systemctlCmd = oldSystemctlCmd
}
}
func Available() error {
_, err := systemctlCmd("--version")
return err
}
var osutilStreamCommand = osutil.StreamCommand
// jctl calls journalctl to get the JSON logs of the given services.
var jctl = func(svcs []string, n string, follow bool) (io.ReadCloser, error) {
// args will need two entries per service, plus a fixed number (give or take
// one) for the initial options.
args := make([]string, 0, 2*len(svcs)+6)
args = append(args, "-o", "json", "-n", n, "--no-pager") // len(this)+1 == that ^ fixed number
if follow {
args = append(args, "-f") // this is the +1 :-)
}
for i := range svcs {
args = append(args, "-u", svcs[i]) // this is why 2×
}
return osutilStreamCommand("journalctl", args...)
}
func MockJournalctl(f func(svcs []string, n string, follow bool) (io.ReadCloser, error)) func() {
oldJctl := jctl
jctl = f
return func() {
jctl = oldJctl
}
}
// Systemd exposes a minimal interface to manage systemd via the systemctl command.
type Systemd interface {
DaemonReload() error
Enable(service string) error
Disable(service string) error
Start(service string) error
Stop(service string, timeout time.Duration) error
Kill(service, signal string) error
Restart(service string, timeout time.Duration) error
Status(services ...string) ([]*ServiceStatus, error)
LogReader(services []string, n string, follow bool) (io.ReadCloser, error)
WriteMountUnitFile(name, what, where, fstype string) (string, error)
Mask(service string) error
Unmask(service string) error
}
// A Log is a single entry in the systemd journal
type Log map[string]string
const (
// the default target for systemd units that we generate
ServicesTarget = "multi-user.target"
// the target prerequisite for systemd units we generate
PrerequisiteTarget = "network-online.target"
// the default target for systemd units that we generate
SocketsTarget = "sockets.target"
)
type reporter interface {
Notify(string)
}
// New returns a Systemd that uses the given rootDir
func New(rootDir string, rep reporter) Systemd {
return &systemd{rootDir: rootDir, reporter: rep}
}
type systemd struct {
rootDir string
reporter reporter
}
// DaemonReload reloads systemd's configuration.
func (*systemd) DaemonReload() error {
_, err := systemctlCmd("daemon-reload")
return err
}
// Enable the given service
func (s *systemd) Enable(serviceName string) error {
_, err := systemctlCmd("--root", s.rootDir, "enable", serviceName)
return err
}
// Unmask the given service
func (s *systemd) Unmask(serviceName string) error {
_, err := systemctlCmd("--root", s.rootDir, "unmask", serviceName)
return err
}
// Disable the given service
func (s *systemd) Disable(serviceName string) error {
_, err := systemctlCmd("--root", s.rootDir, "disable", serviceName)
return err
}
// Mask the given service
func (s *systemd) Mask(serviceName string) error {
_, err := systemctlCmd("--root", s.rootDir, "mask", serviceName)
return err
}
// Start the given service
func (*systemd) Start(serviceName string) error {
_, err := systemctlCmd("start", serviceName)
return err
}
// LogReader for the given services
func (*systemd) LogReader(serviceNames []string, n string, follow bool) (io.ReadCloser, error) {
return jctl(serviceNames, n, follow)
}
var statusregex = regexp.MustCompile(`(?m)^(?:(.+?)=(.*)|(.*))?$`)
type ServiceStatus struct {
Daemon string
ServiceFileName string
Enabled bool
Active bool
}
func (s *systemd) Status(serviceNames ...string) ([]*ServiceStatus, error) {
expected := []string{"Id", "Type", "ActiveState", "UnitFileState"}
cmd := make([]string, len(serviceNames)+2)
cmd[0] = "show"
cmd[1] = "--property=" + strings.Join(expected, ",")
copy(cmd[2:], serviceNames)
bs, err := systemctlCmd(cmd...)
if err != nil {
return nil, err
}
sts := make([]*ServiceStatus, 0, len(serviceNames))
cur := &ServiceStatus{}
seen := map[string]bool{}
for _, bs := range statusregex.FindAllSubmatch(bs, -1) {
if len(bs[0]) == 0 {
// systemctl separates data pertaining to particular services by an empty line
missing := make([]string, 0, len(expected))
for _, k := range expected {
if !seen[k] {
missing = append(missing, k)
}
}
if len(missing) > 0 {
return nil, fmt.Errorf("cannot get service status: missing %s in ‘systemctl show’ output", strings.Join(missing, ", "))
}
sts = append(sts, cur)
if len(sts) > len(serviceNames) {
break // wut
}
if cur.ServiceFileName != serviceNames[len(sts)-1] {
return nil, fmt.Errorf("cannot get service status: queried status of %q but got status of %q", serviceNames[len(sts)-1], cur.ServiceFileName)
}
cur = &ServiceStatus{}
seen = map[string]bool{}
continue
}
if len(bs[3]) > 0 {
return nil, fmt.Errorf("cannot get service status: bad line %q in ‘systemctl show’ output", bs[3])
}
k := string(bs[1])
v := string(bs[2])
if v == "" {
return nil, fmt.Errorf("cannot get service status: empty field %q in ‘systemctl show’ output", k)
}
switch k {
case "Id":
cur.ServiceFileName = v
case "Type":
cur.Daemon = v
case "ActiveState":
// made to match “systemctl is-active” behaviour, at least at systemd 229
cur.Active = v == "active" || v == "reloading"
case "UnitFileState":
// "static" means it can't be disabled
cur.Enabled = v == "enabled" || v == "static"
default:
return nil, fmt.Errorf("cannot get service status: unexpected field %q in ‘systemctl show’ output", k)
}
if seen[k] {
return nil, fmt.Errorf("cannot get service status: duplicate field %q in ‘systemctl show’ output", k)
}
seen[k] = true
}
if len(sts) != len(serviceNames) {
return nil, fmt.Errorf("cannot get service status: expected %d results, got %d", len(serviceNames), len(sts))
}
return sts, nil
}
// Stop the given service, and wait until it has stopped.
func (s *systemd) Stop(serviceName string, timeout time.Duration) error {
if _, err := systemctlCmd("stop", serviceName); err != nil {
return err
}
// and now wait for it to actually stop
giveup := time.NewTimer(timeout)
notify := time.NewTicker(stopNotifyDelay)
defer notify.Stop()
check := time.NewTicker(stopCheckDelay)
defer check.Stop()
firstCheck := true
loop:
for {
select {
case <-giveup.C:
break loop
case <-check.C:
bs, err := systemctlCmd("show", "--property=ActiveState", serviceName)
if err != nil {
return err
}
if isStopDone(bs) {
return nil
}
if !firstCheck {
continue loop
}
firstCheck = false
case <-notify.C:
}
// after notify delay or after a failed first check
s.reporter.Notify(fmt.Sprintf("Waiting for %s to stop.", serviceName))
}
return &Timeout{action: "stop", service: serviceName}
}
// Kill all processes of the unit with the given signal
func (s *systemd) Kill(serviceName, signal string) error {
_, err := systemctlCmd("kill", serviceName, "-s", signal)
return err
}
// Restart the service, waiting for it to stop before starting it again.
func (s *systemd) Restart(serviceName string, timeout time.Duration) error {
if err := s.Stop(serviceName, timeout); err != nil {
return err
}
return s.Start(serviceName)
}
// Error is returned if the systemd action failed
type Error struct {
cmd []string
msg []byte
exitCode int
}
func (e *Error) Error() string {
return fmt.Sprintf("%v failed with exit status %d: %s", e.cmd, e.exitCode, e.msg)
}
// Timeout is returned if the systemd action failed to reach the
// expected state in a reasonable amount of time
type Timeout struct {
action string
service string
}
func (e *Timeout) Error() string {
return fmt.Sprintf("%v failed to %v: timeout", e.service, e.action)
}
// IsTimeout checks whether the given error is a Timeout
func IsTimeout(err error) bool {
_, isTimeout := err.(*Timeout)
return isTimeout
}
// Time returns the time the Log was received by the journal.
func (l Log) Time() (time.Time, error) {
sus, ok := l["__REALTIME_TIMESTAMP"]
if !ok {
return time.Time{}, errors.New("no timestamp")
}
// according to systemd.journal-fields(7) it's microseconds as a decimal string
us, err := strconv.ParseInt(sus, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("timestamp not a decimal number: %#v", sus)
}
return time.Unix(us/1000000, 1000*(us%1000000)).UTC(), nil
}
// Message of the Log, if any; otherwise, "-".
func (l Log) Message() string {
if msg, ok := l["MESSAGE"]; ok {
return msg
}
return "-"
}
// SID is the syslog identifier of the Log, if any; otherwise, "-".
func (l Log) SID() string {
if sid, ok := l["SYSLOG_IDENTIFIER"]; ok {
return sid
}
return "-"
}
// PID is the pid of the client pid, if any; otherwise, "-".
func (l Log) PID() string {
if pid, ok := l["_PID"]; ok {
return pid
}
if pid, ok := l["SYSLOG_PID"]; ok {
return pid
}
return "-"
}
// useFuse detects if we should be using squashfuse instead
func useFuse() bool {
if !osutil.FileExists("/dev/fuse") {
return false
}
_, err := exec.LookPath("squashfuse")
if err != nil {
return false
}
out, err := exec.Command("systemd-detect-virt", "--container").Output()
if err != nil {
return false
}
virt := strings.TrimSpace(string(out))
if virt != "none" {
return true
}
return false
}
// MountUnitPath returns the path of a {,auto}mount unit
func MountUnitPath(baseDir string) string {
escapedPath := EscapeUnitNamePath(baseDir)
return filepath.Join(dirs.SnapServicesDir, escapedPath+".mount")
}
func (s *systemd) WriteMountUnitFile(name, what, where, fstype string) (string, error) {
options := []string{"nodev"}
if fstype == "squashfs" {
options = append(options, "ro", "x-gdu.hide")
}
if osutil.IsDirectory(what) {
options = append(options, "bind")
fstype = "none"
} else if fstype == "squashfs" && useFuse() {
options = append(options, "allow_other")
fstype = "fuse.squashfuse"
}
c := fmt.Sprintf(`[Unit]
Description=Mount unit for %s
Before=snapd.service
[Mount]
What=%s
Where=%s
Type=%s
Options=%s
[Install]
WantedBy=multi-user.target
`, name, what, where, fstype, strings.Join(options, ","))
mu := MountUnitPath(where)
return filepath.Base(mu), osutil.AtomicWriteFile(mu, []byte(c), 0644, 0)
}